feat(server/db): 数据层多库支持(1/4)— 连接分派 + 双方言迁移管线
- config 增 DB_DRIVER(mysql 默认 | sqlite);DSN 对 sqlite 为文件路径
- db.OpenDriver 按驱动分派:sqlite 用 modernc(纯 Go 免 CGO)+ WAL/
busy_timeout/foreign_keys/_txlock=immediate;mysql 路径不变
- store.Open 分派;mysql 保留 UTC/collation 断言,sqlite 跳过
- 迁移拆 migrations/{mysql,sqlite}/ 双套,embed 双 FS,migrate 按驱动选源
与 golang-migrate 驱动;修复 m.Close() 误关调用方 *sql.DB 的坑
- cmd/migrate 串入 DB_DRIVER;集成测试 MigrateUp 签名更新
- 新增 SQLite 时间往返 smoke 测试与端到端迁移测试(免 docker)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
DROP TABLE IF EXISTS devices;
|
||||
DROP TABLE IF EXISTS users;
|
||||
@@ -0,0 +1,22 @@
|
||||
-- 账户与设备(SQLite 方言)
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
uuid TEXT NOT NULL UNIQUE,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
pw_hash TEXT NOT NULL, -- argon2id
|
||||
dp_uuid TEXT NOT NULL, -- 数据面凭证 UUID(可轮换)
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','banned')),
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
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,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
CREATE INDEX idx_devices_user ON devices (user_id);
|
||||
@@ -0,0 +1,2 @@
|
||||
DROP TABLE IF EXISTS subscriptions;
|
||||
DROP TABLE IF EXISTS plans;
|
||||
@@ -0,0 +1,20 @@
|
||||
-- 套餐与订阅
|
||||
CREATE TABLE plans (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code TEXT NOT NULL UNIQUE CHECK (code IN ('free','pro','team')),
|
||||
max_devices INTEGER NOT NULL,
|
||||
daily_minutes INTEGER NULL,
|
||||
ad_gate INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE subscriptions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
plan_id INTEGER NOT NULL,
|
||||
expires_at DATETIME NOT NULL,
|
||||
source TEXT NOT NULL CHECK (source IN ('trial','code')),
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (plan_id) REFERENCES plans(id)
|
||||
);
|
||||
CREATE INDEX idx_subs_user_exp ON subscriptions (user_id, expires_at);
|
||||
@@ -0,0 +1,2 @@
|
||||
DROP TABLE IF EXISTS codes;
|
||||
DROP TABLE IF EXISTS code_batches;
|
||||
@@ -0,0 +1,22 @@
|
||||
-- 激活码
|
||||
CREATE TABLE code_batches (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
channel TEXT NOT NULL CHECK (channel IN ('store','tg','line','manual')),
|
||||
created_by TEXT NOT NULL,
|
||||
note TEXT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE codes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code_hash TEXT NOT NULL UNIQUE, -- SHA-256
|
||||
plan_id INTEGER NOT NULL,
|
||||
duration_days INTEGER NOT NULL,
|
||||
batch_id INTEGER NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'unused' CHECK (status IN ('unused','redeemed','void')),
|
||||
redeemed_by INTEGER NULL,
|
||||
redeemed_at DATETIME NULL,
|
||||
FOREIGN KEY (plan_id) REFERENCES plans(id),
|
||||
FOREIGN KEY (batch_id) REFERENCES code_batches(id)
|
||||
);
|
||||
CREATE INDEX idx_codes_status ON codes (status);
|
||||
@@ -0,0 +1,2 @@
|
||||
DROP TABLE IF EXISTS audit_log;
|
||||
DROP TABLE IF EXISTS usage_daily;
|
||||
@@ -0,0 +1,21 @@
|
||||
-- 用量(仅字节/分钟,无目的地)
|
||||
CREATE TABLE usage_daily (
|
||||
user_id INTEGER NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
bytes_up INTEGER NOT NULL DEFAULT 0,
|
||||
bytes_down INTEGER NOT NULL DEFAULT 0,
|
||||
minutes_used INTEGER NOT NULL DEFAULT 0,
|
||||
ad_unlocked_at DATETIME NULL,
|
||||
PRIMARY KEY (user_id, date)
|
||||
);
|
||||
|
||||
-- 审计
|
||||
CREATE TABLE audit_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
actor TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
target TEXT NOT NULL,
|
||||
meta TEXT NULL, -- JSON
|
||||
at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE INDEX idx_audit_at ON audit_log (at);
|
||||
@@ -0,0 +1,2 @@
|
||||
DROP TABLE IF EXISTS nodes;
|
||||
DROP TABLE IF EXISTS providers;
|
||||
@@ -0,0 +1,33 @@
|
||||
-- VPS 厂商池
|
||||
CREATE TABLE providers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
api_kind TEXT NOT NULL,
|
||||
regions TEXT NOT NULL, -- JSON
|
||||
pool TEXT NOT NULL CHECK (pool IN ('consumable','premium')),
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
note TEXT NULL
|
||||
);
|
||||
|
||||
-- 节点。status CHECK 预置最终全集(含 blocked_*,使 000010 在 SQLite 成为 no-op)。
|
||||
CREATE TABLE nodes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
uuid TEXT NOT NULL UNIQUE,
|
||||
region TEXT NOT NULL,
|
||||
name_zh TEXT NOT NULL,
|
||||
name_en TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'entry' CHECK (role IN ('entry','relay','exit')),
|
||||
tier TEXT NOT NULL CHECK (tier IN ('free','pro')),
|
||||
endpoint TEXT NOT NULL,
|
||||
hy2_port INTEGER NULL,
|
||||
reality_pbk TEXT NOT NULL,
|
||||
reality_sni TEXT NOT NULL,
|
||||
provider_id INTEGER NOT NULL,
|
||||
tags TEXT NULL, -- JSON
|
||||
status TEXT NOT NULL DEFAULT 'provisioning'
|
||||
CHECK (status IN ('provisioning','probing','up','draining','down','destroyed','blocked_suspect','blocked_confirmed')),
|
||||
weight INTEGER NOT NULL DEFAULT 100,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (provider_id) REFERENCES providers(id)
|
||||
);
|
||||
CREATE INDEX idx_nodes_status_tier ON nodes (status, tier);
|
||||
@@ -0,0 +1,2 @@
|
||||
DROP TABLE IF EXISTS directory_version;
|
||||
DROP TABLE IF EXISTS node_events;
|
||||
@@ -0,0 +1,16 @@
|
||||
-- 节点事件。event CHECK 预置最终全集(含 ip_rotated,使 000008 的 enum 扩展在 SQLite 成为 no-op)。
|
||||
CREATE TABLE node_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
node_id INTEGER NOT NULL,
|
||||
event TEXT NOT NULL CHECK (event IN ('provisioned','probe_pass','probe_fail','marked_up','draining','blocked_suspect','blocked_confirmed','replaced','destroyed','ip_rotated')),
|
||||
detail TEXT NULL, -- JSON
|
||||
at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (node_id) REFERENCES nodes(id)
|
||||
);
|
||||
CREATE INDEX idx_node_events_node_at ON node_events (node_id, at);
|
||||
|
||||
-- 目录版本单例
|
||||
CREATE TABLE directory_version (
|
||||
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
|
||||
version INTEGER NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
DELETE FROM directory_version WHERE id = 1;
|
||||
DELETE FROM plans WHERE code IN ('free', 'pro', 'team');
|
||||
@@ -0,0 +1,13 @@
|
||||
-- plans seed(free/pro/team)
|
||||
INSERT INTO plans (code, max_devices, daily_minutes, ad_gate) VALUES
|
||||
('free', 1, 10, 1),
|
||||
('pro', 5, NULL, 0),
|
||||
('team', 10, NULL, 0)
|
||||
ON CONFLICT(code) DO UPDATE SET
|
||||
max_devices = excluded.max_devices,
|
||||
daily_minutes = excluded.daily_minutes,
|
||||
ad_gate = excluded.ad_gate;
|
||||
|
||||
-- directory_version 单例初始化
|
||||
INSERT INTO directory_version (id, version) VALUES (1, 1)
|
||||
ON CONFLICT(id) DO UPDATE SET version = excluded.version;
|
||||
@@ -0,0 +1,5 @@
|
||||
DROP TRIGGER IF EXISTS trg_replacements_updated_at;
|
||||
DROP TABLE IF EXISTS replacements;
|
||||
DROP TABLE IF EXISTS provision_idempotency;
|
||||
ALTER TABLE nodes DROP COLUMN elastic_ip_id;
|
||||
ALTER TABLE nodes DROP COLUMN provider_instance_id;
|
||||
@@ -0,0 +1,33 @@
|
||||
-- 弹性节点基建。附加式变更。
|
||||
ALTER TABLE nodes ADD COLUMN provider_instance_id TEXT NULL;
|
||||
ALTER TABLE nodes ADD COLUMN elastic_ip_id TEXT NULL;
|
||||
|
||||
-- node_events.event 的 'ip_rotated' 已在 SQLite 基线(000006)预置,无需扩展。
|
||||
|
||||
CREATE TABLE provision_idempotency (
|
||||
idempotency_key TEXT NOT NULL PRIMARY KEY,
|
||||
node_uuid TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE replacements (
|
||||
uuid TEXT NOT NULL PRIMARY KEY,
|
||||
old_node_id INTEGER NOT NULL,
|
||||
new_node_id INTEGER NULL,
|
||||
pool TEXT NOT NULL CHECK (pool IN ('consumable','premium')),
|
||||
status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running','done','failed')),
|
||||
step TEXT NOT NULL DEFAULT 'open_new',
|
||||
detail TEXT NULL, -- JSON
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE INDEX idx_repl_status ON replacements (status);
|
||||
CREATE INDEX idx_repl_old_node ON replacements (old_node_id);
|
||||
|
||||
-- 等价 MySQL 的 ON UPDATE CURRENT_TIMESTAMP(recursive_triggers 默认关,不会自递归)。
|
||||
CREATE TRIGGER trg_replacements_updated_at
|
||||
AFTER UPDATE ON replacements
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE replacements SET updated_at = CURRENT_TIMESTAMP WHERE uuid = NEW.uuid;
|
||||
END;
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS admins;
|
||||
@@ -0,0 +1,10 @@
|
||||
-- 管理端账户(密码 argon2id;TOTP 密钥 AES-GCM 后以 BLOB 存储)
|
||||
CREATE TABLE admins (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
pw_hash TEXT NOT NULL,
|
||||
totp_secret BLOB NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active','disabled')),
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login_at DATETIME NULL
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
-- no-op(SQLite):SQLite 无法收紧 CHECK;保留预置全集。
|
||||
@@ -0,0 +1 @@
|
||||
-- no-op(SQLite):nodes.status 的 blocked_suspect/blocked_confirmed 已在基线 000005 的 CHECK 中预置。
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE nodes DROP COLUMN reality_short_id;
|
||||
ALTER TABLE nodes DROP COLUMN reality_prk;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE nodes ADD COLUMN reality_prk TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE nodes ADD COLUMN reality_short_id TEXT NOT NULL DEFAULT '';
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS connect_credentials;
|
||||
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE connect_credentials (
|
||||
node_id INTEGER NOT NULL,
|
||||
dp_uuid TEXT NOT NULL,
|
||||
protocol INTEGER NOT NULL DEFAULT 3, -- agentv1.ProtocolBoth = 3
|
||||
flow TEXT NOT NULL DEFAULT 'xtls-rprx-vision',
|
||||
expires_at DATETIME NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (node_id, dp_uuid),
|
||||
CONSTRAINT fk_cc_node FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_cc_node_expires ON connect_credentials (node_id, expires_at);
|
||||
@@ -0,0 +1,4 @@
|
||||
DROP INDEX IF EXISTS idx_users_sub_token;
|
||||
ALTER TABLE users DROP COLUMN totp_enabled;
|
||||
ALTER TABLE users DROP COLUMN totp_secret_enc;
|
||||
ALTER TABLE users DROP COLUMN sub_token;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- Web 用户中心字段。sub_token 唯一(SQLite 需 ADD COLUMN 后再建唯一索引)。
|
||||
ALTER TABLE users ADD COLUMN sub_token TEXT NULL;
|
||||
ALTER TABLE users ADD COLUMN totp_secret_enc BLOB NULL;
|
||||
ALTER TABLE users ADD COLUMN totp_enabled INTEGER NOT NULL DEFAULT 0;
|
||||
CREATE UNIQUE INDEX idx_users_sub_token ON users (sub_token);
|
||||
Reference in New Issue
Block a user