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:
wangjia
2026-07-10 20:46:13 +08:00
parent 3df5867c02
commit 85140edf1c
6 changed files with 84 additions and 2 deletions
@@ -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)
+3 -2
View File
@@ -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);