e023fb6579
免费版此前形同虚设:ConnectNode 每次连接发固定 daily_minutes×1min TTL、
从不扣减已用,重连即崭新 10 分钟,日额度从未强制。改为账户级(全设备共享)
真卡控 + 累加式看广告加时:
- migration 000020: usage_daily 加 ad_bonus_minutes(sqlite+mysql)。
当日额度 = plans.daily_minutes + ad_bonus_minutes;剩余 = 额度 − minutes_used。
- usage.store.AddAdBonusMinutes: 事务内累加封顶(替代布尔 MarkAdUnlocked),
返回新总额+本次实际加时;GetDay/GetUsageRange 补读 ad_bonus_minutes。
- usage.service.UnlockAd: verify+nonce 后 +adBonusPerAd(10) 封顶 adDailyBonusCeiling(120),
返回 granted+remaining;TodaySummary 加 MinutesCap/AdBonusMinutes。
- usage.ads DevVerifier: 放行式占位校验(nonce 防重放仍生效),接真 AdMob 前
让看广告加时流程可端到端跑通;main.go 按 ADS_DEV_MODE/默认装配。
- ads/unlock: 204 → 200 返回 {granted_minutes, minutes_remaining}。
- ConnectNode 免费门: 读 AccountDayMinutes(used,bonus) → remaining≤0 拒 QUOTA_EXHAUSTED,
否则 TTL=remaining(凭证到点硬切断兜底)。nodes.store 加 AccountDayMinutes。
- /me: 补 ad_bonus_minutes,quota_today_min 分母改 daily+bonus,加 quota_cap_min。
- quota.CheckFreeConnect 同步累加语义(免费基础 10 分钟不再需先看广告)。
- openapi + 契约/迁移版本/集成测试同步;新增 SQLite AddAdBonusMinutes 单测。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
62 lines
1.7 KiB
Go
62 lines
1.7 KiB
Go
package httpapi
|
|
|
|
// contract_test.go — HTTP 契约快照(支柱 2:契约单源)。
|
|
//
|
|
// 冻结 GET /v1/me 响应的字段面(JSON key)。后端加/删/改字段会让本测试失败,逼开发者
|
|
// 同步客户端 model + 文档,避免控制面与客户端悄悄分叉。
|
|
//
|
|
// ⚠️ 与客户端冻结集对账:client/test/contract/api_contract_test.dart 的 _frozenMeKeys
|
|
// (9 个)。后端这里多发 uuid / dp_uuid(客户端故意忽略);expire_at 已废弃移除——
|
|
// 无 web 用户中心后,两端统一只用 expires_at。改任一端都要回看另一端。
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func jsonTagSet(v any) map[string]struct{} {
|
|
t := reflect.TypeOf(v)
|
|
if t.Kind() == reflect.Ptr {
|
|
t = t.Elem()
|
|
}
|
|
set := map[string]struct{}{}
|
|
for i := 0; i < t.NumField(); i++ {
|
|
tag := t.Field(i).Tag.Get("json")
|
|
if tag == "" || tag == "-" {
|
|
continue
|
|
}
|
|
name := strings.SplitN(tag, ",", 2)[0]
|
|
if name != "" {
|
|
set[name] = struct{}{}
|
|
}
|
|
}
|
|
return set
|
|
}
|
|
|
|
func assertFrozenKeys(t *testing.T, name string, got map[string]struct{}, want ...string) {
|
|
t.Helper()
|
|
wantSet := make(map[string]struct{}, len(want))
|
|
for _, k := range want {
|
|
wantSet[k] = struct{}{}
|
|
}
|
|
for k := range got {
|
|
if _, ok := wantSet[k]; !ok {
|
|
t.Errorf("%s: 出现未冻结字段 %q —— 改契约需同步另一端(客户端 model)+ 文档", name, k)
|
|
}
|
|
}
|
|
for _, k := range want {
|
|
if _, ok := got[k]; !ok {
|
|
t.Errorf("%s: 缺失冻结字段 %q", name, k)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestContractMeResponse(t *testing.T) {
|
|
assertFrozenKeys(t, "/v1/me", jsonTagSet(meResponse{}),
|
|
"uuid", "email", "dp_uuid", "plan", "expires_at",
|
|
"devices_used", "devices_max",
|
|
"quota_today_min", "quota_cap_min", "data_today_gb", "weekly_gb", "totp_enabled",
|
|
)
|
|
}
|