feat(server/pay): 订单列表端点 + 分币种展示价 + 富字段详情
- GET /v1/pay/orders 订单列表(Store.ListByUser,本地台账倒序) - catalog 增 PriceUsdtMicro(USDT 一口价展示) + SettlementCurrency/DisplayAmountMinor - 下单按支付方式落展示金额/结算币种(crypto→USDT / 法币→CNY),webhook 覆盖实际 - GetOrder 详情回带 sku/plan/method/amount/currency/created_at/paid_at - 新增 List 越权/倒序/富字段 + 分币种展示金额 + 详情富字段测试 - 修 3 个 pre-existing 迁移测试(renumber 后 21→23 遗留) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
This commit is contained in:
@@ -19,8 +19,8 @@ import (
|
||||
// migratorAt builds a golang-migrate instance against a file-backed SQLite DB
|
||||
// and steps it to exactly `version` (unlike store.MigrateUp/Down, which always
|
||||
// target head/0). This is Task 8 Step 2's upgrade rehearsal: a real file DB
|
||||
// (not :memory:) simulating a production sqlite store carrying pre-000021 rows
|
||||
// through the 000021 subscriptions-table rebuild.
|
||||
// (not :memory:) simulating a production sqlite store carrying pre-000023 rows
|
||||
// through the 000023 subscriptions-table rebuild.
|
||||
func migratorAt(t *testing.T, database *sql.DB, version uint) {
|
||||
t.Helper()
|
||||
src, err := iofs.New(migrations.SQLiteFS, "sqlite")
|
||||
@@ -44,7 +44,7 @@ func migratorAt(t *testing.T, database *sql.DB, version uint) {
|
||||
}
|
||||
}
|
||||
|
||||
// insertLegacySubRow inserts one subscriptions row with a pre-000021 source
|
||||
// insertLegacySubRow inserts one subscriptions row with a pre-000023 source
|
||||
// value ('trial' or 'code') for a fresh user, returning the assigned user_id
|
||||
// and subscription id.
|
||||
func insertLegacySubRow(t *testing.T, database *sql.DB, uuidSuffix, source string) (userID, subID int64) {
|
||||
@@ -82,7 +82,7 @@ func insertLegacySubRow(t *testing.T, database *sql.DB, uuidSuffix, source strin
|
||||
}
|
||||
|
||||
// TestSQLitePayMigrationRehearsal_UpgradeWithData is Task 8 Step 2: rehearse
|
||||
// the 000021 upgrade (subscriptions table rebuild for source='pay') against a
|
||||
// the 000023 upgrade (subscriptions table rebuild for source='pay') against a
|
||||
// file-backed SQLite DB pre-loaded with real 'trial'/'code' rows, and assert
|
||||
// no rows are lost and the id/AUTOINCREMENT sequence is preserved.
|
||||
func TestSQLitePayMigrationRehearsal_UpgradeWithData(t *testing.T) {
|
||||
@@ -101,15 +101,15 @@ func TestSQLitePayMigrationRehearsal_UpgradeWithData(t *testing.T) {
|
||||
_ = trialUserID
|
||||
_ = codeUserID
|
||||
|
||||
// 2. Up to 000021 — subscriptions_new rebuild + pay_purchases creation.
|
||||
migratorAt(t, database, 21)
|
||||
// 2. Up to 000023 — subscriptions_new rebuild + pay_purchases creation.
|
||||
migratorAt(t, database, 23)
|
||||
|
||||
v, dirty, err := store.MigrateVersion(database, "sqlite")
|
||||
if err != nil {
|
||||
t.Fatalf("MigrateVersion: %v", err)
|
||||
}
|
||||
if dirty || v != 21 {
|
||||
t.Fatalf("after up to 21: version=%d dirty=%v, want 21/false", v, dirty)
|
||||
if dirty || v != 23 {
|
||||
t.Fatalf("after up to 23: version=%d dirty=%v, want 23/false", v, dirty)
|
||||
}
|
||||
|
||||
// 3. Row count and ids preserved across the rebuild.
|
||||
@@ -118,7 +118,7 @@ func TestSQLitePayMigrationRehearsal_UpgradeWithData(t *testing.T) {
|
||||
t.Fatalf("count subscriptions: %v", err)
|
||||
}
|
||||
if count != 2 {
|
||||
t.Errorf("subscriptions count after 000021 = %d, want 2 (rows lost in rebuild)", count)
|
||||
t.Errorf("subscriptions count after 000023 = %d, want 2 (rows lost in rebuild)", count)
|
||||
}
|
||||
|
||||
for _, want := range []struct {
|
||||
@@ -127,7 +127,7 @@ func TestSQLitePayMigrationRehearsal_UpgradeWithData(t *testing.T) {
|
||||
}{{trialSubID, "trial"}, {codeSubID, "code"}} {
|
||||
var gotSource string
|
||||
if err := database.QueryRow(`SELECT source FROM subscriptions WHERE id = ?`, want.id).Scan(&gotSource); err != nil {
|
||||
t.Errorf("subscription id=%d missing after 000021: %v", want.id, err)
|
||||
t.Errorf("subscription id=%d missing after 000023: %v", want.id, err)
|
||||
continue
|
||||
}
|
||||
if gotSource != want.source {
|
||||
@@ -146,7 +146,7 @@ func TestSQLitePayMigrationRehearsal_UpgradeWithData(t *testing.T) {
|
||||
trialUserID, planID, time.Now().Add(30*24*time.Hour).UTC().Format("2006-01-02 15:04:05"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("insert source='pay' subscription after 000021: %v", err)
|
||||
t.Fatalf("insert source='pay' subscription after 000023: %v", err)
|
||||
}
|
||||
paySubID, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
@@ -156,10 +156,10 @@ func TestSQLitePayMigrationRehearsal_UpgradeWithData(t *testing.T) {
|
||||
t.Errorf("pay subscription id=%d did not continue AUTOINCREMENT sequence (prior max id=%d)", paySubID, codeSubID)
|
||||
}
|
||||
|
||||
// 5. pay_purchases table exists and is empty (fresh table from 000021).
|
||||
// 5. pay_purchases table exists and is empty (fresh table from 000023).
|
||||
var payCount int
|
||||
if err := database.QueryRow(`SELECT COUNT(*) FROM pay_purchases`).Scan(&payCount); err != nil {
|
||||
t.Fatalf("count pay_purchases (table should exist post-000021): %v", err)
|
||||
t.Fatalf("count pay_purchases (table should exist post-000023): %v", err)
|
||||
}
|
||||
if payCount != 0 {
|
||||
t.Errorf("pay_purchases count = %d, want 0 (fresh table)", payCount)
|
||||
@@ -167,11 +167,11 @@ func TestSQLitePayMigrationRehearsal_UpgradeWithData(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestSQLitePayMigrationRehearsal_DownUpIdempotent covers the second half of
|
||||
// Task 8 Step 2: with only pre-000021 ('trial'/'code') data present, down
|
||||
// (rollback 000021) then up (re-apply) must be idempotent and lossless.
|
||||
// Task 8 Step 2: with only pre-000023 ('trial'/'code') data present, down
|
||||
// (rollback 000023) then up (re-apply) must be idempotent and lossless.
|
||||
//
|
||||
// It also documents an intentional safety property: once a source='pay' row
|
||||
// exists, 000021's down.sql (which rebuilds subscriptions with the stricter
|
||||
// exists, 000023's down.sql (which rebuilds subscriptions with the stricter
|
||||
// CHECK (source IN ('trial','code'))) correctly REFUSES to downgrade rather
|
||||
// than silently dropping paid-subscription rows — see the trailing assertion.
|
||||
func TestSQLitePayMigrationRehearsal_DownUpIdempotent(t *testing.T) {
|
||||
@@ -186,9 +186,9 @@ func TestSQLitePayMigrationRehearsal_DownUpIdempotent(t *testing.T) {
|
||||
_, trialSubID := insertLegacySubRow(t, database, "trial2", "trial")
|
||||
_, codeSubID := insertLegacySubRow(t, database, "code2", "code")
|
||||
|
||||
migratorAt(t, database, 21)
|
||||
migratorAt(t, database, 23)
|
||||
|
||||
// down: 000021 -> 000020. No 'pay' rows exist yet, so this must succeed
|
||||
// down: 000023 -> 000020. No 'pay' rows exist yet, so this must succeed
|
||||
// and preserve the trial/code rows with their original ids.
|
||||
migratorAt(t, database, 20)
|
||||
|
||||
@@ -219,14 +219,14 @@ func TestSQLitePayMigrationRehearsal_DownUpIdempotent(t *testing.T) {
|
||||
t.Errorf("pay_purchases table still present after down to 000020 (err=%v)", err)
|
||||
}
|
||||
|
||||
// up: 000020 -> 000021 again — idempotent re-apply, same rows/ids.
|
||||
migratorAt(t, database, 21)
|
||||
// up: 000020 -> 000023 again — idempotent re-apply, same rows/ids.
|
||||
migratorAt(t, database, 23)
|
||||
v, dirty, err = store.MigrateVersion(database, "sqlite")
|
||||
if err != nil {
|
||||
t.Fatalf("MigrateVersion after re-up: %v", err)
|
||||
}
|
||||
if dirty || v != 21 {
|
||||
t.Fatalf("after re-up to 21: version=%d dirty=%v, want 21/false", v, dirty)
|
||||
if dirty || v != 23 {
|
||||
t.Fatalf("after re-up to 23: version=%d dirty=%v, want 23/false", v, dirty)
|
||||
}
|
||||
if err := database.QueryRow(`SELECT COUNT(*) FROM subscriptions`).Scan(&count); err != nil {
|
||||
t.Fatalf("count subscriptions after re-up: %v", err)
|
||||
@@ -237,7 +237,7 @@ func TestSQLitePayMigrationRehearsal_DownUpIdempotent(t *testing.T) {
|
||||
|
||||
// Safety-net documentation: once a source='pay' row exists, down must be
|
||||
// refused (CHECK (source IN ('trial','code')) on the down-rebuilt table),
|
||||
// not silently drop it. This is NOT a bug — see 000021.down.sql.
|
||||
// not silently drop it. This is NOT a bug — see 000023.down.sql.
|
||||
var planID int64
|
||||
if err := database.QueryRow(`SELECT id FROM plans WHERE code = 'pro'`).Scan(&planID); err != nil {
|
||||
t.Fatalf("lookup pro plan id: %v", err)
|
||||
|
||||
@@ -29,8 +29,8 @@ func TestSQLiteMigrateUpDown(t *testing.T) {
|
||||
if dirty {
|
||||
t.Fatalf("schema dirty after MigrateUp")
|
||||
}
|
||||
if v != 21 {
|
||||
t.Errorf("version = %d, want 21", v)
|
||||
if v != 23 {
|
||||
t.Errorf("version = %d, want 23", v)
|
||||
}
|
||||
|
||||
// 2. Core tables exist.
|
||||
|
||||
Reference in New Issue
Block a user