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:
wangjia
2026-06-28 18:13:49 +08:00
parent b98ad9dce4
commit 1a6eb7ac9a
4 changed files with 619 additions and 0 deletions
@@ -0,0 +1,163 @@
# Pangolin 统计体系整改 Implementation Plantodo #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、SwiftiOS/macOS NEPacketTunnelProvider + libbox)、KotlinAndroid VpnService + libbox)、Go(控制面 + agent)、sing-box libbox、SQLite/MySQLdialect 层)。
**设计依据:** `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+CommandGroupstatusInterval 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`(镜像 iOSapp 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_pagemobile/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)。