feat(server): pro 套餐设备上限 5 → 3

- seed(sqlite/mysql 000007)+ 001_init:pro max_devices 5→3(全新库直接对)
- 新增迁移 000019(sqlite/mysql,up/down):UPDATE plans SET max_devices=3
  WHERE code='pro' —— 已迁移的线上库靠它更新;down 回退 5
- 000002/001 注释同步(free 1 / pro 3 / team 10)
- 测试:sqlite_migrate 期望版本 18→19;devices 集成测试 pro 断言 5→3
- free(1)/team(10)不变

注:设备上限当前仍未强制执行(登录注册 MaxDevices=0),仅数据/展示口径;
真正启用+超限 UX 见 todo #16。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-01 18:07:03 +08:00
parent 58211d2fb8
commit 27dc59ed63
10 changed files with 19 additions and 11 deletions
@@ -172,15 +172,15 @@ func TestFullChain(t *testing.T) {
ctx := context.Background() ctx := context.Background()
userID := createUser(t, db, "chain@example.com", "active") userID := createUser(t, db, "chain@example.com", "active")
// 7-day pro trial → effective plan pro (max 5). // 7-day pro trial → effective plan pro (max 3).
giveSubscription(t, db, userID, "pro", "trial", time.Now().UTC().Add(7*24*time.Hour)) giveSubscription(t, db, userID, "pro", "trial", time.Now().UTC().Add(7*24*time.Hour))
plan, apiErr := svc.ResolvePlan(ctx, userID) plan, apiErr := svc.ResolvePlan(ctx, userID)
if apiErr != nil { if apiErr != nil {
t.Fatalf("ResolvePlan: %v", apiErr) t.Fatalf("ResolvePlan: %v", apiErr)
} }
if plan.PlanCode != "pro" || plan.MaxDevices != 5 { if plan.PlanCode != "pro" || plan.MaxDevices != 3 {
t.Fatalf("want pro/5, got %s/%d", plan.PlanCode, plan.MaxDevices) t.Fatalf("want pro/3, got %s/%d", plan.PlanCode, plan.MaxDevices)
} }
devUUID := newUUID(t, db) devUUID := newUUID(t, db)
+2 -2
View File
@@ -29,8 +29,8 @@ func TestSQLiteMigrateUpDown(t *testing.T) {
if dirty { if dirty {
t.Fatalf("schema dirty after MigrateUp") t.Fatalf("schema dirty after MigrateUp")
} }
if v != 18 { if v != 19 {
t.Errorf("version = %d, want 18", v) t.Errorf("version = %d, want 19", v)
} }
// 2. Core tables exist. // 2. Core tables exist.
+2 -2
View File
@@ -25,14 +25,14 @@ CREATE TABLE IF NOT EXISTS users (
CREATE TABLE IF NOT EXISTS plans ( CREATE TABLE IF NOT EXISTS plans (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
code ENUM('free','pro','team') NOT NULL UNIQUE, code ENUM('free','pro','team') NOT NULL UNIQUE,
max_devices INT NOT NULL, -- free 1 / pro 5 / team 10 max_devices INT NOT NULL, -- free 1 / pro 3 / team 10
daily_minutes INT NULL, -- free 10; NULL = unlimited daily_minutes INT NULL, -- free 10; NULL = unlimited
ad_gate BOOLEAN NOT NULL DEFAULT FALSE -- free users must watch an ad daily ad_gate BOOLEAN NOT NULL DEFAULT FALSE -- free users must watch an ad daily
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT IGNORE INTO plans (code, max_devices, daily_minutes, ad_gate) VALUES INSERT IGNORE INTO plans (code, max_devices, daily_minutes, ad_gate) VALUES
('free', 1, 10, TRUE), ('free', 1, 10, TRUE),
('pro', 5, NULL, FALSE), ('pro', 3, NULL, FALSE),
('team', 10, NULL, FALSE); ('team', 10, NULL, FALSE);
-- ------------------------------------------------------------------------- -- -------------------------------------------------------------------------
@@ -2,7 +2,7 @@
CREATE TABLE plans ( CREATE TABLE plans (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
code ENUM('free','pro','team') NOT NULL UNIQUE, code ENUM('free','pro','team') NOT NULL UNIQUE,
max_devices INT NOT NULL, -- free 1 / pro 5 / team 10 max_devices INT NOT NULL, -- free 1 / pro 3 / team 10
daily_minutes INT NULL, -- free 10NULL = 不限 daily_minutes INT NULL, -- free 10NULL = 不限
ad_gate BOOLEAN NOT NULL DEFAULT FALSE -- free 每日看广告解锁 ad_gate BOOLEAN NOT NULL DEFAULT FALSE -- free 每日看广告解锁
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+2 -2
View File
@@ -1,11 +1,11 @@
-- plans seed(口径来源:design/CLAUDE.md §7,价格不入表) -- plans seed(口径来源:design/CLAUDE.md §7,价格不入表)
-- free: 1 设备,10 分钟/天,每日广告解锁 -- free: 1 设备,10 分钟/天,每日广告解锁
-- pro: 5 设备,不限时长,无广告门槛 -- pro: 3 设备,不限时长,无广告门槛
-- team: 10 设备,不限时长,无广告门槛 -- team: 10 设备,不限时长,无广告门槛
INSERT INTO plans (code, max_devices, daily_minutes, ad_gate) INSERT INTO plans (code, max_devices, daily_minutes, ad_gate)
VALUES VALUES
('free', 1, 10, TRUE), ('free', 1, 10, TRUE),
('pro', 5, NULL, FALSE), ('pro', 3, NULL, FALSE),
('team', 10, NULL, FALSE) ('team', 10, NULL, FALSE)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
max_devices = VALUES(max_devices), max_devices = VALUES(max_devices),
@@ -0,0 +1,2 @@
-- 回退:pro 套餐设备上限 3 → 5。
UPDATE plans SET max_devices = 5 WHERE code = 'pro';
@@ -0,0 +1,2 @@
-- pro 套餐设备上限 5 → 3(已迁移库更新;新库由 000007_seed 直接 seed 为 3)。
UPDATE plans SET max_devices = 3 WHERE code = 'pro';
+1 -1
View File
@@ -1,7 +1,7 @@
-- plans seedfree/pro/team -- plans seedfree/pro/team
INSERT INTO plans (code, max_devices, daily_minutes, ad_gate) VALUES INSERT INTO plans (code, max_devices, daily_minutes, ad_gate) VALUES
('free', 1, 10, 1), ('free', 1, 10, 1),
('pro', 5, NULL, 0), ('pro', 3, NULL, 0),
('team', 10, NULL, 0) ('team', 10, NULL, 0)
ON CONFLICT(code) DO UPDATE SET ON CONFLICT(code) DO UPDATE SET
max_devices = excluded.max_devices, max_devices = excluded.max_devices,
@@ -0,0 +1,2 @@
-- 回退:pro 套餐设备上限 3 → 5。
UPDATE plans SET max_devices = 5 WHERE code = 'pro';
@@ -0,0 +1,2 @@
-- pro 套餐设备上限 5 → 3(已迁移库更新;新库由 000007_seed 直接 seed 为 3)。
UPDATE plans SET max_devices = 3 WHERE code = 'pro';