docs(stats): stats-overhaul 计划/测试清单 + index 登记
- stats-overhaul 实现计划(md 真相源 + html 阅读版) - 功能×测试覆盖清单(feature-test-coverage-checklist.html) - docs/index.html 登记上述文档 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
# Pangolin 统计体系整改 Implementation Plan(todo #5)
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 四端实时统计真生效(上传/下载/延迟/累计流量)+ 服务端按设备归因并按 GB 账户综合卡控 + 统计页按新设计(上聚合·下分设备)像素级还原。
|
||||
|
||||
**Architecture:** Flutter UI 全平台共享。实时指标经冻结的 `pangolin/vpn/stats` channel 由各原生侧(libbox CommandClient / Clash API)生产;累计用量由服务端 `/v1/usage`、`/v1/me` 提供。服务端控制面(Go,mysql/sqlite 双驱动)经 agent gRPC `ReportUsage` 按 `dp_uuid` 记账;本计划把记账粒度从「账户」细化到「设备」(每设备独立 dp_uuid),并把配额单位从分钟改 GB、按账户综合卡控。
|
||||
|
||||
**Tech Stack:** Flutter/Dart、Swift(iOS/macOS NEPacketTunnelProvider + libbox)、Kotlin(Android VpnService + libbox)、Go(控制面 + agent)、sing-box libbox、SQLite/MySQL(dialect 层)。
|
||||
|
||||
**设计依据:** `design/CONTRACT.md`、`design/ui_kits/{mobile,tablet,desktop}`、`docs/ios-ipad-support-design.html`;探查结论见审批计划 `~/.claude/plans/crystalline-exploring-lerdorf.md`。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Channel 契约冻结:`pangolin/vpn/stats` 字段 = uploadBytes/downloadBytes/uploadSpeed/downloadSpeed/urltestResults(`client/lib/bridge/vpn_bridge.dart:27-37`),**不得私改**。
|
||||
- 配置由服务端渲染下发,客户端绝不拼配置。
|
||||
- 服务端可移植铁律:走 `dialect.Upsert/LockForUpdate`(`server/internal/db/dialect.go`),时间 Go 端算,禁 `UTC_TIMESTAMP()/NOW()/FIELD()` 等 MySQL 专属;迁移 `server/migrations/{mysql,sqlite}` 双份。
|
||||
- token 真相源单源:只改 `design/colors_and_type.css` + 跑 `node design/codegen/gen_flutter_tokens.mjs`;`client/lib/pangolin_tokens.gen.dart` 勿手改。
|
||||
- 统一元素:复用/提升 `client/lib/widgets/`,禁就地造元素;颜色禁硬编码 hex,走语义 token。
|
||||
- 文案禁红线词(VPN/翻墙/科学上网…),用「加速」口径,过 `ci/scan-redline.sh`;双语单显不并排。
|
||||
- iOS App Group `group.com.pangolin.pangolinVpn`;macOS App Group `BYL4KQHMTN.com.pangolin.pangolin`。
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — 四端实时统计修复(无 schema 改动)
|
||||
|
||||
让上传/下载/延迟/累计流量在四端全部可见。优先级最高、风险最低、见效最快。
|
||||
|
||||
### Task 1.1: 服务端记账核实(先确认数据源)
|
||||
|
||||
**Files:** `server/internal/httpapi/clientconfig.go`、`server/internal/agentd/usage*.go`、`server/internal/nodes/handler_grpc.go`。
|
||||
|
||||
- [x] **Step 1:** 已核实——节点配置 `render.go` 启用 `experimental.v2ray_api.stats`(`enabled:true`+`users:statsUsers(creds)`),inbound 用户 `name=dp_uuid`;agent `usage_v2ray.go` 按 `user>>>{dp_uuid}>>>traffic` 精确读取。链路代码完整正确。
|
||||
- [x] **Step 2:** 已核实——`handler_grpc.go:282-313` `dp_uuid→user_id` → `store.go AccumulateUsage` 累加 `usage_daily(user_id,date)`,全程 dialect 可移植。
|
||||
- [ ] **Step 3(运行时):** 「累计流量没数据」非代码缺陷,是运行时产物(需真实连接产生被记账的流量 / 确认节点 build 含 `with_v2ray_api`)——真机连一次验证。**利好**:dp_uuid 改每设备后这套命名天然按设备拆分,Phase 2 节点+agent 侧近零改动。
|
||||
|
||||
### Task 1.2: iOS 实时 stats 生产者(核心)
|
||||
|
||||
**Files:** `client/ios/PacketTunnel/PacketTunnelProvider.swift`、`client/ios/Runner/VpnManager.swift`、`client/ios/Runner/AppDelegate.swift`。
|
||||
|
||||
**Interfaces:** Produces → `pangolin/vpn/stats` EventChannel 真实帧。
|
||||
|
||||
- [x] **Step 1:** 新增 `client/ios/Runner/StatsClient.swift`——`LibboxNewCommandClient`(CommandStatus+CommandGroup,statusInterval 1s)连 App Group 容器(`LibboxSetup` 同扩展路径),含 10 次重试。
|
||||
- [x] **Step 2:** `writeStatus` 映射 uplinkTotal/downlinkTotal→uploadBytes/downloadBytes、uplink/downlink→uploadSpeed/downloadSpeed。
|
||||
- [x] **Step 3:** `writeGroups` 遍历组成员 `urlTestDelay`→契约 urltestResults。
|
||||
- [x] **Step 4:** 经 `VpnStatsStreamHandler.shared.push` 推 EventChannel;`VpnManager` 状态观察 connected→start / disconnected→stop;断开推零帧。pbxproj 四处登记(plutil OK)。
|
||||
- [ ] **Step 5(真机):** Release build 装真机,连接后验证上传/下载/延迟实时跳动。
|
||||
|
||||
### Task 1.3: macOS 实时 stats 生产者
|
||||
|
||||
**Files:** `client/macos/Runner/VpnChannel.swift`、`client/macos/PacketTunnel/PacketTunnelProvider.swift`。
|
||||
|
||||
- [x] **Step 1:** 新增 `client/macos/Runner/StatsClient.swift`(镜像 iOS,app group `BYL4KQHMTN.com.pangolin.pangolin`);替换 `VpnChannel.swift` 旧占位(旧帧 schema 也错,推 up/down 非契约字段)。
|
||||
- [x] **Step 2:** `VpnChannel` 状态观察驱动 start/stop,`onStats` 回调推 statsSink;`onStatsListen` 去掉占位 Timer。pbxproj 四处登记(plutil OK)。
|
||||
- [ ] **Step 2 验证(真机):** 递增 CFBundleVersion + 公证流程(CLAUDE.md),装机验证。
|
||||
|
||||
### Task 1.4: Android 延迟修复
|
||||
|
||||
**Files:** `client/android/app/src/main/kotlin/com/pangolin/pangolin_vpn/PangolinVpnService.kt`。
|
||||
|
||||
- [x] **Step 1(代码核查):** `PangolinVpnService.kt` 已 `addCommand(CommandGroup)`、`writeGroups` 正确解析 `getURLTestDelay()`——读取链路无缺陷。
|
||||
- [x] **Step 2(主动探测修复):** 补偿 sing-box urltest 惰性探测——`writeGroups` 捕获 `groupTags`,新增 `startUrlTestTimer()`(`java.util.Timer` 每 12s 对各组 `client.urlTest(tag)`),`doStop` 取消。让延迟稳定及时出现,不等 3min 默认 interval。同样的主动触发也加到了 iOS/macOS `StatsClient`(`urlTestTimer`)。
|
||||
- [ ] **Step 3(真机):** 装机验证延迟出现。
|
||||
|
||||
### Task 1.5: Windows 延迟修复
|
||||
|
||||
**Files:** `client/lib/bridge/kernel_process.dart`、(必要时)`server/internal/httpapi/clientconfig.go`。
|
||||
|
||||
- [x] **Step 1(根因定位+修复):** 真正 bug——`getGroupDelay()` 早已实现却**从未被调用**,轮询只读 `/proxies` 缓存,urltest 惰性探测下 `history` 长期为空 → 延迟空白。修复:`_startStatsPoll` 每 `_urlTestEvery=12` 拍对提取出的 `_urltestGroups` fire-and-forget `getGroupDelay()` 主动触发探测;新增 `extractUrltestGroups()` 提取 URLTest/Fallback 组名。
|
||||
- [x] **Step 2(单测):** `kernel_process_test.dart` 新增「URLTest extraction」组 3 用例(组名提取/最新 history delay/空组)——`flutter test` 26 全过。
|
||||
- [ ] **Step 3(真机):** Windows 装机验证延迟出现。
|
||||
|
||||
### Task 1.6: Phase 1 验收
|
||||
|
||||
- [ ] `flutter analyze` + `flutter test`(含现有 golden)通过。
|
||||
- [ ] 四端连接后:上传/下载/延迟实时跳动;累计流量 + 周柱图有值。
|
||||
- [ ] 提交,合并验收后再进 Phase 2。
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — 每设备归因 + GB 综合配额(后端重型)
|
||||
|
||||
### Task 2.1: Schema 迁移(mysql + sqlite 双份)
|
||||
|
||||
**Files:** `server/migrations/{mysql,sqlite}/0000X_*.up/down.sql`。
|
||||
|
||||
- [x] **Step 1:** `devices.dp_uuid`(sqlite TEXT / mysql CHAR(36),NULL)+ 独立 `CREATE UNIQUE INDEX idx_devices_dp_uuid`(SQLite ADD COLUMN 不支持内联 UNIQUE,两库对齐)。
|
||||
- [x] **Step 2:** 新表 `usage_device_daily`,PK `(device_id, date)`,索引 `(user_id, date)`。
|
||||
- [x] **Step 3:** `plans.daily_mb`(MB 整数,支持免费 500MB;与 `daily_minutes` 并存);seed free 500 / pro 102400(100GB) / team 204800(200GB),按用户确认数值。
|
||||
- [x] **Step 4:** `000015_per_device_usage.{up,down}.sql` 双驱动;sqlite `TestSQLiteMigrateUpDown` up+down 干净(版本→15、新表/列断言已加),`run_sqlite_test.sh` 全过、`go build ./...` OK。mysql 侧待 `run_mysql_test.sh`(需 docker)。
|
||||
|
||||
### Task 2.2: 每设备 dp_uuid 链路
|
||||
|
||||
**Files:** `server/internal/devices/service.go`、`server/internal/httpapi/nodes.go`、`server/internal/nodes/store.go`、`server/proto/agent/v1/agent.proto`、`server/internal/nodes/handler_grpc.go`。
|
||||
|
||||
- [x] **Step 1:** `store.EnsureDeviceDpUUID(userID, deviceUUID)`——connect 时按设备**懒 mint** `devices.dp_uuid`(`idgen.NewString()`,guarded UPDATE + re-read 处理并发竞态)。
|
||||
- [x] **Step 2:** `ConnectNode` 用设备 dp_uuid 建 cred + 渲染 config(`EnsureDeviceDpUUID` 失败则**回退账户 dp_uuid**,不破坏未注册设备的旧客户端)。
|
||||
- [x] **Step 3:** `store.UserDeviceByDpUUID(dpUUID) → (user, device)`——优先查 `devices.dp_uuid`,回退 legacy `users.dp_uuid`(device=0)。
|
||||
- [x] **Step 4:** `ReportUsage` 双写——账户 `AccumulateUsage` + 设备 `AccumulateDeviceUsage`(仅 device>0)。
|
||||
- [x] **Step 5:** 兼容——懒 mint 使存量设备下次 connect 自动补发;legacy 账户 dp_uuid 仍解析(device=0,不归设备)。节点侧零改动:每设备 dp_uuid 作独立 connect_credential → `render.go statsUsers`/inbound 天然按设备拆。
|
||||
- [x] **验证:** `sqlite_per_device_test.go` 3 用例(mint 幂等/区分/未知设备报错、dp_uuid→(user,device) 解析+账户回退、设备用量累加分离)+ `grpc_test.go TestReportUsage_PerDevice`(双写)/`TestReportUsage_Accumulates`(账户级 device=0 不写设备)——`go test ./...` 全过。
|
||||
|
||||
### Task 2.3: GB 综合配额卡控
|
||||
|
||||
**Files:** `server/internal/usage/quota.go`、`server/internal/httpapi/nodes.go`。
|
||||
|
||||
- [x] **决策(用户):** 免费=看广告解锁+分钟与 GB **双卡**;免费 500MB/天;付费高 GB 上限(pro 100GB/team 200GB)。
|
||||
- [x] **Step 1:** GB 综合卡控落在**真实 connect 路径** `ConnectNode`(`CheckFreeConnect` 实为未接线死代码)——`ent.DailyMB.Valid` 时取 `store.AccountDayBytes`(账户当日 up+down),`≥ daily_mb*1MB` 即拒 403 `QUOTA_EXHAUSTED`;免费/付费统一。`EntitlementForUser` 加载 `daily_mb`(free 回退 500)。
|
||||
- [x] **验证:** `TestSQLite_AccountDayBytes`(求和/隔离日期)+ `TestSQLite_EntitlementForUser_DailyMB`(free 回退 500 / pro 订阅 102400)——全过。
|
||||
- [ ] **Step 2(后续):** 复用 `ReportUsage` 周期路径跨阈值 revoke 凭证,实现近持续卡控(当前仅 connect 时卡)。
|
||||
- [ ] **Step 3(后续):** 完整 ad-unlock 门控接线(ConnectNode 现为 MVP「flat 10min」,未强制看广告);`/v1/me` quota 语义改 GB(并入 Task 2.4)。
|
||||
|
||||
### Task 2.4: 按设备用量 API ✅
|
||||
|
||||
**Files:** `server/internal/usage/{store,service,handler}.go`、`server/cmd/server/main.go`、`client/lib/models/device_usage.dart`、`client/lib/services/account_api.dart`、`client/lib/state/account_providers.dart`。
|
||||
|
||||
- [x] **Step 1:** 新增 `GET /v1/usage/devices?days=N`(独立端点,复用 `auth.RequireAuth` group)。后端:`usage.Store.DeviceUsageRange`(JOIN devices + GROUP BY 设备,窗口求和,busiest-first,可移植 SQL)→ `Service.DeviceUsage` → `DeviceUsageHandler`,返回 `{devices:[{uuid,name,platform,bytes_up,bytes_down,minutes_used}]}`。
|
||||
- [x] **Step 2:** 客户端 model `DeviceUsage` + `AccountApi.deviceUsage({days})` + `deviceUsageProvider`(family days)。UI 渲染留到 Phase 3(设计先行)。
|
||||
- [x] **验证:** sqlite 实库测试 `TestSQLite_DeviceUsageRange`(分组求和/窗口隔离/账户隔离/busiest-first/JOIN 元数据);handler guard 测试 `TestDeviceUsageHandler_Guards`(401/405/400);Dart `device_usage_test.dart`(fromJson + 默认值)。全量 `go test ./...` 绿、`flutter test` 146 过。
|
||||
|
||||
### Task 2.5: Phase 2 验收
|
||||
|
||||
- [ ] sqlite 实库测试(`go test ./...` + `./server/run_sqlite_test.sh`)覆盖每设备累加、综合卡控、dp_uuid 解析。
|
||||
- [ ] mysql 集成测试(`./server/run_mysql_test.sh`)通过。
|
||||
- [ ] 端到端两设备分别连接、用量各归各账;GB 超额被拒。
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — 统计页重设计(design-first)+ 四端实现
|
||||
|
||||
### Task 3.1: 分设备明细设计(design-source 同步)✅
|
||||
|
||||
**Files:** `design/ui_kits/{mobile,tablet,desktop}/*`、`design/CONTRACT.md`。
|
||||
|
||||
- [x] **Step 1:** 「设备明细 / By device」section 加入三端原型(mobile `screens.jsx StatsScreen`、tablet `tabapp.jsx TabStats`、desktop `dapp.jsx DStats`):上聚合沿用现 3 指标卡 + 周柱;下分设备列表 = 平台 Lucide 图标盒 38(accent-subtle) + 名称 + 占比迷你条(height 4, accent .85) + 流量(mono)/时长。字串入各端 dict(`byDevice`/`noDeviceUsage`,双语)。
|
||||
- [x] **Step 2:** `design/CONTRACT.md` §3.3 增分设备明细像素规格 + 数据源标注。
|
||||
|
||||
### Task 3.2: 统一组件提升 ✅
|
||||
|
||||
**Files:** `client/lib/widgets/metric_card.dart`(新)、`client/lib/widgets/device_stat_row.dart`(新)。
|
||||
|
||||
- [x] **Step 1:** `stats_page.dart` 私有 `_MetricCard` → 公共 `widgets/metric_card.dart`(`MetricCard`)。
|
||||
- [x] **Step 2:** 新增 `widgets/device_stat_row.dart`(`DeviceStatRow` + 静态 `iconFor` 平台图标,与 account_screens 同一视觉语言)。
|
||||
|
||||
### Task 3.3: 四端 stats_page 接线 ✅
|
||||
|
||||
**Files:** `client/lib/screens/stats_page.dart`、`client/lib/l10n/*`。
|
||||
|
||||
- [x] **Step 1:** stats_page(mobile/tablet 窄宽分支 + desktop isWide)接 `deviceUsageProvider(30)`;上聚合下分设备(`_DeviceDetail`);占比条按最忙设备归一;全程语义 token(占比条 accent/border,无硬编码 hex)。
|
||||
- [x] **Step 2:** 文案入 `l10n/app_text.dart` + `strings_{zh,en}.dart`(`byDevice`/`noDeviceUsage`),双语单显,无红线词(「设备明细」中性)。
|
||||
|
||||
### Task 3.4: Phase 3 验收 ✅
|
||||
|
||||
- [x] golden 回归闸(CONTRACT §4 既定 pixel gate):tablet 注入 demo 设备用量 → `tablet_stats_{light_zh,light_en,dark_zh}` 重生;desktop 零数据态 → `desktop_stats` 重生(空态 section)。
|
||||
- [x] `flutter analyze` 无 error/warning;`flutter test` **146 过**;`go test ./...` 绿。
|
||||
- [ ] 真机/模拟器多端目测(待用户验证)。
|
||||
|
||||
---
|
||||
|
||||
## 不在本次范围
|
||||
|
||||
- 组织级多租户层级(父子账户)——user==account 已满足。
|
||||
- 协议选择(todo #8)、KillSwitch(#1/#2/#3)。
|
||||
Reference in New Issue
Block a user