← 返回文档索引

免费版 10 分钟卡控 + 累加式看广告加时

设计 · todo #21 · 后端 + 前端 + 数据库 · 额度全账户共享

免费版此前形同虚设:连接页无倒计时、时间到不卡控、按钮永远可点。根因在服务端—— ConnectNode 免费门每次连接都发固定 daily_minutes×1min TTL、从不扣减已用, 重连即崭新 10 分钟,日额度从未真正强制。本次将其改为账户级(全设备共享)真卡控 + 累加式看广告加时:连接期倒计时、到点自动切断、耗尽按钮灰化、点击弹广告、看完 +N 分钟。

目标与口径(已确认)

数据模型

migration 000020_ad_bonus_minutes(sqlite + mysql)

ALTER TABLE usage_daily ADD COLUMN ad_bonus_minutes INT NOT NULL DEFAULT 0;

当日免费额度 = plans.daily_minutes(free=10)+ usage_daily.ad_bonus_minutes(看广告累加); 剩余 = 额度 − minutes_used。历史列 ad_unlocked_at(布尔式当日解锁)保留但不再用于卡控。

后端

改动
usage/store.goAddAdBonusMinutes(uid,day,add,ceiling):事务内 LockForUpdate 读旧值 → 累加封顶 → upsert,返回新总额 + 本次实际加时DailyUsage/GetDay/GetUsageRange 补读 ad_bonus_minutesMarkAdUnlocked 标 deprecated。
usage/service.go常量 adBonusPerAd=10 / adDailyBonusCeiling=120TodaySummaryMinutesCap(=daily+bonus)/ AdBonusMinutesremaining=cap−usedUnlockAd 改累加:verify+nonce 后 +adBonusPerAd 封顶,返回 (granted, remaining)
usage/ads.go + main.go放行式 DevVerifierVerify 恒 nil)。main.goADS_DEV_MODE/默认装配(替换现在的 nil——否则 UnlockAd 直接 ErrInternal,占位流程走不通)。nonce 防重放仍生效。
httpapi/nodes.go ConnectNode新增 nodes.store.AccountDayMinutes(uid,day)→(used,bonus)。免费门:allowance=daily+bonusremaining=allowance−usedremaining≤0→拒 QUOTA_EXHAUSTED;否则 TTL=remaining×1min(凭证到点硬切断兜底,防绕过客户端)。
httpapi/account.go /me补读 ad_bonus_minutesquota_today_min 分母改 daily+bonus;新增 quota_cap_min(=allowance,客户端进度条分母)。
usage/handler.goPOST /v1/ads/unlock:204 → 200 返回 {granted_minutes, minutes_remaining}

前端(四端共享 Dart)

改动
models/me.dartquotaCapMin(当日总额度)。
state/quota_provider.darttotal=me.quotaCapMinisExhaustedmarkExhausted()(倒计时归零本地置耗尽 + 登录态拉 me 校准);watchAd() async 调 /ads/unlock(占位 ad_token=uuid)→ 刷新 me,返回 granted。
state/connection_provider.dart连接时锁定 _freeRemainingSec(会员为 null 不倒计时)。复用 elapsed 计时器每 tick 算 countdown=cap−elapsed,写入 ConnectionState.freeCountdown归零→自动切断_onFreeQuotaExhausted:主动断开不报节点异常 + markExhausted)。倒计时用墙上时钟,后台漏跳回前台补上、准时切。连接遇后端 QUOTA_EXHAUSTED 兜底置耗尽。
widgets/connect_button.dartenabled/onDisabledTap:off 态额度耗尽 → 灰化(锁图标),点击走加时流程。
widgets/quota_card.dart三态:连接中显示倒计时 mm:ss + 进度收缩;未连接有余额显示剩余分钟;耗尽显示“今日已用完”。移动端「看广告加时」/ 桌面「升级会员」。
widgets/ad_reward_dialog.dart(新)移动端占位广告:“广告播放中…”→3s→调 watchAd→显示“已加 N 分钟”自动关闭。桌面版:升级/移动端提示弹窗(无广告)。
l10n(zh/en)倒计时/今日已用完/看广告加时/占位广告播放/奖励/桌面升级提示 双语文案。

时序

移动端典型流

连接 → /me remaining=10 → 倒计时 10:00 …… 00:00
  → 客户端 _onFreeQuotaExhausted:切断隧道 + 按钮灰化 + markExhausted
点灰按钮 → 占位广告 dialog(3s)→ POST /ads/unlock(DevVerifier 放行 + nonce)
  → 服务端 ad_bonus_minutes += 10(封顶 120)→ 返回 granted=10, remaining=10
  → 刷新 /me(quota_cap_min=20, quota_today_min=10)→ 按钮恢复可连
再次连接 → ConnectNode remaining=allowance−used → TTL=remaining(服务端硬切断兜底)

桌面:同样倒计时 + 到点切断 + 灰按钮,但点击弹“去移动端看广告或升级会员”,无加时路径(硬 10 分钟/天)。

验证

不在本轮