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:
wangjia
2026-07-11 23:45:21 +08:00
parent 43c76fb978
commit a5adbf347b
8 changed files with 273 additions and 52 deletions
@@ -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)
+2 -2
View File
@@ -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.