feat(server): 迁移 000021 pay_purchases 台账 + subscriptions.source 扩 pay
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
This commit is contained in:
@@ -104,6 +104,12 @@ func TestCodesLibMigrateRoundTrip(t *testing.T) {
|
||||
// the reviewer's repro hit: the lib's `codes` table collides with the
|
||||
// name 000020's down script renames legacy_codes back to.
|
||||
m := newSQLiteStepper(t, db)
|
||||
// 000021 (pay_purchases/source-enum) now sits on top of 000020 and is
|
||||
// unrelated to this collision — step it back down first so we land
|
||||
// exactly on the 000020 boundary the test targets.
|
||||
if err := m.Steps(-1); err != nil {
|
||||
t.Fatalf("step 000021 down: %v", err)
|
||||
}
|
||||
if err := m.Steps(-1); err != nil {
|
||||
t.Fatalf("step 000020 down: %v (this is the reviewer-reported collision — "+
|
||||
"000020 down must DROP the codes-lib tables before renaming legacy_* back)", err)
|
||||
|
||||
@@ -29,8 +29,8 @@ func TestSQLiteMigrateUpDown(t *testing.T) {
|
||||
if dirty {
|
||||
t.Fatalf("schema dirty after MigrateUp")
|
||||
}
|
||||
if v != 20 {
|
||||
t.Errorf("version = %d, want 20", v)
|
||||
if v != 21 {
|
||||
t.Errorf("version = %d, want 21", v)
|
||||
}
|
||||
|
||||
// 2. Core tables exist.
|
||||
@@ -39,6 +39,7 @@ func TestSQLiteMigrateUpDown(t *testing.T) {
|
||||
"usage_daily", "audit_log", "providers", "nodes", "node_events",
|
||||
"directory_version", "provision_idempotency", "replacements", "admins",
|
||||
"connect_credentials", "usage_device_daily", "sessions", "usage_hourly", "usage_device_hourly",
|
||||
"pay_purchases",
|
||||
} {
|
||||
var name string
|
||||
err := db.QueryRow(
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
DROP TABLE IF EXISTS pay_purchases;
|
||||
-- 注意:若已存在 source='pay' 的行,该 MODIFY 会失败——down 前需人工清理(标准 enum 收窄语义)。
|
||||
ALTER TABLE subscriptions MODIFY source ENUM('trial','code') NOT NULL;
|
||||
@@ -0,0 +1,21 @@
|
||||
-- pay v2 接入:订阅来源扩 'pay' + 购买台账(biz_ref↔out_trade_no 映射 + webhook 幂等消费)
|
||||
ALTER TABLE subscriptions MODIFY source ENUM('trial','code','pay') NOT NULL;
|
||||
|
||||
CREATE TABLE pay_purchases (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
biz_ref CHAR(36) NOT NULL, -- users.uuid 冗余(webhook 兜底定位)
|
||||
sku VARCHAR(64) NOT NULL, -- pay products.biz_code
|
||||
out_trade_no VARCHAR(64) NOT NULL UNIQUE, -- pay 订单号 = 幂等键
|
||||
method VARCHAR(32) NOT NULL, -- alipay | crypto(retry 换渠道时更新)
|
||||
status ENUM('created','paid','canceled') NOT NULL DEFAULT 'created',
|
||||
amount_minor BIGINT NOT NULL DEFAULT 0, -- webhook 回填,最小单位
|
||||
currency VARCHAR(16) NOT NULL DEFAULT '',
|
||||
channel VARCHAR(32) NOT NULL DEFAULT '',
|
||||
sub_id BIGINT UNSIGNED NULL, -- 开通/延长的 subscriptions.id
|
||||
paid_at DATETIME(6) NULL,
|
||||
created_at DATETIME(6) NOT NULL,
|
||||
updated_at DATETIME(6) NOT NULL,
|
||||
INDEX idx_pay_user (user_id, created_at),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@@ -0,0 +1,16 @@
|
||||
DROP TABLE IF EXISTS pay_purchases;
|
||||
CREATE TABLE subscriptions_old (
|
||||
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)
|
||||
);
|
||||
INSERT INTO subscriptions_old (id, user_id, plan_id, expires_at, source, created_at)
|
||||
SELECT id, user_id, plan_id, expires_at, source, created_at FROM subscriptions;
|
||||
DROP TABLE subscriptions;
|
||||
ALTER TABLE subscriptions_old RENAME TO subscriptions;
|
||||
CREATE INDEX idx_subs_user_exp ON subscriptions (user_id, expires_at);
|
||||
@@ -0,0 +1,35 @@
|
||||
-- 订阅 source 扩 'pay':重建表(CHECK 约束不可 ALTER)
|
||||
CREATE TABLE subscriptions_new (
|
||||
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','pay')),
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (plan_id) REFERENCES plans(id)
|
||||
);
|
||||
INSERT INTO subscriptions_new (id, user_id, plan_id, expires_at, source, created_at)
|
||||
SELECT id, user_id, plan_id, expires_at, source, created_at FROM subscriptions;
|
||||
DROP TABLE subscriptions;
|
||||
ALTER TABLE subscriptions_new RENAME TO subscriptions;
|
||||
CREATE INDEX idx_subs_user_exp ON subscriptions (user_id, expires_at);
|
||||
|
||||
CREATE TABLE pay_purchases (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
biz_ref TEXT NOT NULL,
|
||||
sku TEXT NOT NULL,
|
||||
out_trade_no TEXT NOT NULL UNIQUE,
|
||||
method TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'created' CHECK (status IN ('created','paid','canceled')),
|
||||
amount_minor INTEGER NOT NULL DEFAULT 0,
|
||||
currency TEXT NOT NULL DEFAULT '',
|
||||
channel TEXT NOT NULL DEFAULT '',
|
||||
sub_id INTEGER NULL,
|
||||
paid_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
CREATE INDEX idx_pay_user ON pay_purchases (user_id, created_at);
|
||||
Reference in New Issue
Block a user