Pangolin · 真正启用套餐设备上限 + 非硬拒登的「移除设备」体验 · 2026-07-01
plans.max_devices(free 1 / pro 3 / team 10)过去只展示、不强制:登录注册写死
MaxDevices=0,CheckDeviceQuota 是死代码。本次真正启用上限,按卡在登录(非硬拒登)
+ 选择移除/一键踢最旧 + 服务端自动清理久不活跃落地。
| 维度 | 选择 | 理由 |
|---|---|---|
| 执行点 | 登录(+ 连接 backstop 待硬化) | 登录照常签发 token,超限只返回 device_limit 信号、客户端挡在「移除设备」页;不硬拒登。 |
| 解决方式 | 选择移除 + 一键「移除最久未用」 | 兼顾控制与便捷;移除即调现成 DeleteDevice(撤销会话+删行+召回 dp_uuid 凭证→设备立即离线登出)。 |
| 免费重装 churn | 服务端自动清理久不活跃 | count 只算近 staleWindow=30d(last_seen)活跃设备;登录 best-effort 删超期僵尸行 → 重装刷新的 UUID 自愈,不锁死用户。 |
无 schema 变更:复用 plans.max_devices、devices.last_seen、sessions。仅新增常量 staleWindow=30d。
活跃计数 SELECT COUNT(1) FROM devices WHERE user_id=? AND last_seen IS NOT NULL AND last_seen > now-30d。
另:本次把 pro 上限从 5 改为 3(seed + 迁移 000019;线上需 migrate up 生效)。
internal/devices
store.CountActiveDevices(近 30d)+ store.PruneStaleDevices(删超期僵尸行,best-effort)。service.CheckDeviceLimit:prune → ResolvePlan → 活跃 count > cap 即 Over,附活跃设备列表。设备登录时已注册,故「超」= activeCount > cap。internal/auth(登录闸,非硬拒登)
RegisterIfAbsent(MaxDevices=0) + 建 session(不动 best-effort)。DeviceRegistrar 加 CheckDeviceLimit;recordLogin 回传 *DeviceLimit;LoginOutcome.DeviceLimit;Login 透传;handler tokenPairResponse.device_limit(omitempty)。main.go 的 authDeviceRegistrar 适配 devices.CheckDeviceLimit → auth.DeviceLimit。登录响应(超限时):
{ "access_token":"…", "refresh_token":"…", "expires_in":900,
"device_limit": { "max_devices":1,
"devices":[ {"uuid":"…","name":"iPhone","platform":"ios","last_seen":"2026-06-30T…Z"} ] } }
待硬化 连接 backstop(ConnectNode 拒超限)本轮从简未做:登录闸是服务端权威信号、客户端据此自挡;后续在 Entitlement + NodeStore 加 count 做服务端硬拦。
auth_api.dart:AuthTokens 解析 device_limit;新增 DeviceLimit/DeviceBrief 模型。auth_provider.dart:deviceLimitProvider(挡板状态);auth_screen 登录后置入。main.dart:isLoggedIn 后若 deviceLimit!=null → DeviceLimitScreen(挡 HomeShell)。screens/device_limit_screen.dart:设备列表(本机标注、不可删)+ 每台移除 + 一键「移除最久未用」(取 last_seen 最旧的非本机)+ 退出登录;移除后 devicesProvider 刷新,count≤max 自动清挡板放行。deviceLimitTitle/Desc/RemoveOldest(zh/en)。devices within/over/prune-stale(TestCheckDeviceLimit,integration 标签,需 Docker/CI);auth 登录透传超限信号(仍签发 token)。flutter analyze 干净;client 全量测试 161 通过。last_seen 改到 30d 前 → 不计数且被自动清理。