diff --git a/server/migrations/mysql/000020_ad_bonus_minutes.down.sql b/server/migrations/mysql/000020_ad_bonus_minutes.down.sql new file mode 100644 index 0000000..e659a14 --- /dev/null +++ b/server/migrations/mysql/000020_ad_bonus_minutes.down.sql @@ -0,0 +1 @@ +ALTER TABLE usage_daily DROP COLUMN ad_bonus_minutes; diff --git a/server/migrations/mysql/000020_ad_bonus_minutes.up.sql b/server/migrations/mysql/000020_ad_bonus_minutes.up.sql new file mode 100644 index 0000000..17e0257 --- /dev/null +++ b/server/migrations/mysql/000020_ad_bonus_minutes.up.sql @@ -0,0 +1,4 @@ +-- 免费版累加式看广告加时:当日通过看激励视频额外解锁的分钟数(与 minutes_used 相对)。 +-- 当日免费额度 = plans.daily_minutes + usage_daily.ad_bonus_minutes;剩余 = 额度 − minutes_used。 +-- 历史列 ad_unlocked_at(布尔式当日解锁)保留但不再用于卡控。 +ALTER TABLE usage_daily ADD COLUMN ad_bonus_minutes INT NOT NULL DEFAULT 0; diff --git a/server/migrations/mysql/000021_devices_user_scoped_uuid.down.sql b/server/migrations/mysql/000021_devices_user_scoped_uuid.down.sql new file mode 100644 index 0000000..0f902fe --- /dev/null +++ b/server/migrations/mysql/000021_devices_user_scoped_uuid.down.sql @@ -0,0 +1,12 @@ +-- 回退:UNIQUE(user_id,uuid) → 全局 UNIQUE(uuid),platform ENUM 去掉 linux。 +-- 有损:同一 uuid 跨用户的重复行只保留最早一行(id 最小),linux 平台行删除。 + +DELETE d1 FROM devices d1 +JOIN devices d2 ON d1.uuid = d2.uuid AND d1.id > d2.id; + +DELETE FROM devices WHERE platform = 'linux'; + +ALTER TABLE devices + DROP INDEX uniq_devices_user_uuid, + ADD UNIQUE KEY uuid (uuid), + MODIFY platform ENUM('ios','android','windows','macos') NOT NULL; diff --git a/server/migrations/mysql/000021_devices_user_scoped_uuid.up.sql b/server/migrations/mysql/000021_devices_user_scoped_uuid.up.sql new file mode 100644 index 0000000..be80874 --- /dev/null +++ b/server/migrations/mysql/000021_devices_user_scoped_uuid.up.sql @@ -0,0 +1,11 @@ +-- 设备唯一键 UNIQUE(uuid) → UNIQUE(user_id, uuid)(F3:同机换账号 403 死结) +-- +-- device_id 是客户端按「安装」生成并持久的,跨账号复用;全局 UNIQUE(uuid) 使同一台 +-- 机器登第二个账号时 RegisterIfAbsent 永远 403。改为按用户隔离:同一物理设备在每个 +-- 账号下各有一行。顺手给 platform ENUM 加 linux(normalizePlatform 已接受,列会拒)。 +-- MySQL 可原地 ALTER,无需重建(列级 UNIQUE 的隐式索引名 = 列名 uuid)。 + +ALTER TABLE devices + DROP INDEX uuid, + ADD UNIQUE KEY uniq_devices_user_uuid (user_id, uuid), + MODIFY platform ENUM('ios','android','windows','macos','linux') NOT NULL; diff --git a/server/migrations/mysql/000020_codes_lib_legacy_rename.down.sql b/server/migrations/mysql/000022_codes_lib_legacy_rename.down.sql similarity index 100% rename from server/migrations/mysql/000020_codes_lib_legacy_rename.down.sql rename to server/migrations/mysql/000022_codes_lib_legacy_rename.down.sql diff --git a/server/migrations/mysql/000020_codes_lib_legacy_rename.up.sql b/server/migrations/mysql/000022_codes_lib_legacy_rename.up.sql similarity index 100% rename from server/migrations/mysql/000020_codes_lib_legacy_rename.up.sql rename to server/migrations/mysql/000022_codes_lib_legacy_rename.up.sql diff --git a/server/migrations/mysql/000021_pay_purchases.down.sql b/server/migrations/mysql/000023_pay_purchases.down.sql similarity index 100% rename from server/migrations/mysql/000021_pay_purchases.down.sql rename to server/migrations/mysql/000023_pay_purchases.down.sql diff --git a/server/migrations/mysql/000021_pay_purchases.up.sql b/server/migrations/mysql/000023_pay_purchases.up.sql similarity index 100% rename from server/migrations/mysql/000021_pay_purchases.up.sql rename to server/migrations/mysql/000023_pay_purchases.up.sql diff --git a/server/migrations/sqlite/000020_ad_bonus_minutes.down.sql b/server/migrations/sqlite/000020_ad_bonus_minutes.down.sql new file mode 100644 index 0000000..e659a14 --- /dev/null +++ b/server/migrations/sqlite/000020_ad_bonus_minutes.down.sql @@ -0,0 +1 @@ +ALTER TABLE usage_daily DROP COLUMN ad_bonus_minutes; diff --git a/server/migrations/sqlite/000020_ad_bonus_minutes.up.sql b/server/migrations/sqlite/000020_ad_bonus_minutes.up.sql new file mode 100644 index 0000000..2482cab --- /dev/null +++ b/server/migrations/sqlite/000020_ad_bonus_minutes.up.sql @@ -0,0 +1,4 @@ +-- 免费版累加式看广告加时:当日通过看激励视频额外解锁的分钟数(与 minutes_used 相对)。 +-- 当日免费额度 = plans.daily_minutes + usage_daily.ad_bonus_minutes;剩余 = 额度 − minutes_used。 +-- 历史列 ad_unlocked_at(布尔式当日解锁)保留但不再用于卡控。 +ALTER TABLE usage_daily ADD COLUMN ad_bonus_minutes INTEGER NOT NULL DEFAULT 0; diff --git a/server/migrations/sqlite/000021_devices_user_scoped_uuid.down.sql b/server/migrations/sqlite/000021_devices_user_scoped_uuid.down.sql new file mode 100644 index 0000000..87bb901 --- /dev/null +++ b/server/migrations/sqlite/000021_devices_user_scoped_uuid.down.sql @@ -0,0 +1,59 @@ +-- 回退:UNIQUE(user_id,uuid) → 全局 UNIQUE(uuid),platform CHECK 去掉 linux。 +-- 有损:同一 uuid 跨用户的重复行只保留最早一行(id 最小),linux 平台行删除 +-- (旧 CHECK 不接受)。次序与 up 相同(rename → 重建 → 复制 → drop)。 + +ALTER TABLE devices RENAME TO devices_old; + +CREATE TABLE devices ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uuid TEXT NOT NULL UNIQUE, + user_id INTEGER NOT NULL, + name TEXT NOT NULL, + platform TEXT NOT NULL CHECK (platform IN ('ios','android','windows','macos')), + last_seen DATETIME NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + client_version TEXT NULL, + totp_trusted_until DATETIME NULL, + dp_uuid TEXT NULL, + FOREIGN KEY (user_id) REFERENCES users(id) +); + +INSERT INTO devices (id, uuid, user_id, name, platform, last_seen, created_at, + client_version, totp_trusted_until, dp_uuid) +SELECT id, uuid, user_id, name, platform, last_seen, created_at, + client_version, totp_trusted_until, dp_uuid +FROM devices_old o +WHERE o.platform IN ('ios','android','windows','macos') + AND o.id = (SELECT MIN(i.id) FROM devices_old i WHERE i.uuid = o.uuid); + +ALTER TABLE sessions RENAME TO sessions_old; + +CREATE TABLE sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + device_id INTEGER NOT NULL, + refresh_jti TEXT NOT NULL UNIQUE, + client_ip TEXT NULL, + client_version TEXT NULL, + created_at DATETIME NOT NULL, + last_active DATETIME NULL, + revoked_at DATETIME NULL, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE +); + +-- 只回填仍存在对应设备行的会话(被去重删掉的设备,其会话一并丢弃)。 +INSERT INTO sessions (id, user_id, device_id, refresh_jti, client_ip, client_version, + created_at, last_active, revoked_at) +SELECT s.id, s.user_id, s.device_id, s.refresh_jti, s.client_ip, s.client_version, + s.created_at, s.last_active, s.revoked_at +FROM sessions_old s +WHERE EXISTS (SELECT 1 FROM devices d WHERE d.id = s.device_id); + +DROP TABLE sessions_old; +DROP TABLE devices_old; + +CREATE INDEX idx_devices_user ON devices (user_id); +CREATE UNIQUE INDEX idx_devices_dp_uuid ON devices (dp_uuid); +CREATE INDEX idx_sessions_user ON sessions (user_id); +CREATE INDEX idx_sessions_device ON sessions (device_id); diff --git a/server/migrations/sqlite/000021_devices_user_scoped_uuid.up.sql b/server/migrations/sqlite/000021_devices_user_scoped_uuid.up.sql new file mode 100644 index 0000000..4ae5c7a --- /dev/null +++ b/server/migrations/sqlite/000021_devices_user_scoped_uuid.up.sql @@ -0,0 +1,68 @@ +-- 设备唯一键 UNIQUE(uuid) → UNIQUE(user_id, uuid)(F3:同机换账号 403 死结) +-- +-- device_id 是客户端按「安装」生成并持久的,跨账号复用;全局 UNIQUE(uuid) 使同一台 +-- 机器登第二个账号时 RegisterIfAbsent 永远 403 → ConnectNode 判 DEVICE_NOT_REGISTERED, +-- 且提示的「重新登录」无法自救。改为按用户隔离:同一物理设备在每个账号下各有一行。 +-- 顺手落地 migration 16 头注释里推迟的两件事:本唯一键改造 + platform CHECK 加 linux +-- (normalizePlatform 已接受 linux,但旧 CHECK 会拒 INSERT)。 +-- +-- SQLite 无法删除列级 UNIQUE,须重建表。sessions 对 devices 有 ON DELETE CASCADE 外键 +-- 且连接开启 foreign_keys=ON:直接 DROP devices 会级联清空全部会话(=全员被登出)。 +-- 安全次序(单事务内成立,不依赖 PRAGMA foreign_keys=OFF): +-- ① RENAME devices → devices_old(sessions 的 FK 定义随 rename 跟走到 devices_old); +-- ② 建新 devices(复合唯一键),按原 id 复制数据; +-- ③ RENAME sessions → sessions_old,建新 sessions(FK 指向新 devices),复制数据; +-- ④ 先 DROP 子表 sessions_old,再 DROP 已无引用的 devices_old;重建全部索引。 + +ALTER TABLE devices RENAME TO devices_old; + +CREATE TABLE devices ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uuid TEXT NOT NULL, + user_id INTEGER NOT NULL, + name TEXT NOT NULL, + platform TEXT NOT NULL CHECK (platform IN ('ios','android','windows','macos','linux')), + last_seen DATETIME NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + client_version TEXT NULL, + totp_trusted_until DATETIME NULL, + dp_uuid TEXT NULL, + UNIQUE (user_id, uuid), + FOREIGN KEY (user_id) REFERENCES users(id) +); + +INSERT INTO devices (id, uuid, user_id, name, platform, last_seen, created_at, + client_version, totp_trusted_until, dp_uuid) +SELECT id, uuid, user_id, name, platform, last_seen, created_at, + client_version, totp_trusted_until, dp_uuid +FROM devices_old; + +ALTER TABLE sessions RENAME TO sessions_old; + +CREATE TABLE sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + device_id INTEGER NOT NULL, + refresh_jti TEXT NOT NULL UNIQUE, + client_ip TEXT NULL, + client_version TEXT NULL, + created_at DATETIME NOT NULL, + last_active DATETIME NULL, + revoked_at DATETIME NULL, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE +); + +INSERT INTO sessions (id, user_id, device_id, refresh_jti, client_ip, client_version, + created_at, last_active, revoked_at) +SELECT id, user_id, device_id, refresh_jti, client_ip, client_version, + created_at, last_active, revoked_at +FROM sessions_old; + +DROP TABLE sessions_old; +DROP TABLE devices_old; + +CREATE INDEX idx_devices_user ON devices (user_id); +CREATE UNIQUE INDEX idx_devices_dp_uuid ON devices (dp_uuid); +CREATE INDEX idx_sessions_user ON sessions (user_id); +CREATE INDEX idx_sessions_device ON sessions (device_id); diff --git a/server/migrations/sqlite/000020_codes_lib_legacy_rename.down.sql b/server/migrations/sqlite/000022_codes_lib_legacy_rename.down.sql similarity index 100% rename from server/migrations/sqlite/000020_codes_lib_legacy_rename.down.sql rename to server/migrations/sqlite/000022_codes_lib_legacy_rename.down.sql diff --git a/server/migrations/sqlite/000020_codes_lib_legacy_rename.up.sql b/server/migrations/sqlite/000022_codes_lib_legacy_rename.up.sql similarity index 100% rename from server/migrations/sqlite/000020_codes_lib_legacy_rename.up.sql rename to server/migrations/sqlite/000022_codes_lib_legacy_rename.up.sql diff --git a/server/migrations/sqlite/000021_pay_purchases.down.sql b/server/migrations/sqlite/000023_pay_purchases.down.sql similarity index 100% rename from server/migrations/sqlite/000021_pay_purchases.down.sql rename to server/migrations/sqlite/000023_pay_purchases.down.sql diff --git a/server/migrations/sqlite/000021_pay_purchases.up.sql b/server/migrations/sqlite/000023_pay_purchases.up.sql similarity index 100% rename from server/migrations/sqlite/000021_pay_purchases.up.sql rename to server/migrations/sqlite/000023_pay_purchases.up.sql