topfans/backend/services/assetService/service/peripheral_test_helpers_test.go
zerosaturation 3407e30395 refactor(assetService): CreateMintOrder 3-phase txn + orphan reconciliation (P0-2)
- CreateMintOrder 拆三段: txn1(PENDING→PROCESSING,不调RPC/不写asset)
  → 事务外 UpdateCrystalBalance 扣水晶 → txn2(建asset+registry→SUCCESS);
  消除 DB 事务内嵌跨服务 gRPC(连接池占用+跨服务事务风险)。
- 任一步失败 markMintOrderFailed 独立事务标 FAILED(不回滚已扣水晶=审计流水,
  重试靠 Task1 source_id 幂等 + Task2 入口短路防双扣); nil-cause 防御。
- 孤儿订单对账 ReconcileStuckMintOrders: 扫陈旧 PROCESSING 单,查 mint_cost 流水—
  已扣未建→FOR UPDATE 行锁下幂等补完 SUCCESS(串行化并发防双建),未扣→FAILED,绝不退款。
- main.go 接线周期 worker(MINT_RECONCILE_INTERVAL_SEC 默认600s,0关,优雅退出)。
- 测试: RPC失败落FAILED / 对账未扣→FAILED / 已扣未建→补完(含幂等二次跑)。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-23 12:28:57 +08:00

134 lines
5.1 KiB
Go

package service
import (
"testing"
"github.com/topfans/backend/pkg/database"
"github.com/topfans/backend/pkg/models"
"gorm.io/gorm"
)
// setupServiceTestDB 服务层测试用 DB helper
//
// 沿用 backend/services/assetService/repository/asset_repository_test.go 的 setupTestDB 风格,
// 但因为 helpers 是包内私有,服务层需要自己的副本。注意:
// - DB 配置对齐 docker/.env.local(localhost:15432, postgres/123456, top-fans)
// - AutoMigrate 包含 PeripheralInfo(peripheral_info 表)
// - 修复 local DB 缺 stars 表:raw SQL CREATE TABLE IF NOT EXISTS + ALTER ADD COLUMN IF NOT EXISTS
// - CREATE UNIQUE INDEX,兼容已有"残缺" stars 表的情况
func setupServiceTestDB(t *testing.T) *gorm.DB {
t.Helper()
config := database.Config{
Host: "localhost",
Port: 15432,
User: "postgres",
Password: "123456",
DBName: "top-fans",
SSLMode: "disable",
TimeZone: "Asia/Shanghai",
}
if err := database.Init(config); err != nil {
t.Skipf("Skipping test: failed to connect to test database: %v", err)
}
db := database.GetDB()
db.Migrator().DropTable(&models.PeripheralInfo{})
if err := db.AutoMigrate(&models.Asset{}, &models.MintOrder{}, &models.AssetLike{}, &models.PeripheralInfo{}, &models.AssetRegistry{}); err != nil {
t.Logf("Warning: Failed to migrate asset tables (may already exist): %v", err)
}
db.Exec(`CREATE TABLE IF NOT EXISTS stars (
star_id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
tag VARCHAR(100),
name_en VARCHAR(500),
pic_url VARCHAR(500),
description TEXT,
identity_id VARCHAR(50) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT true,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL
)`)
db.Exec("ALTER TABLE stars ADD COLUMN IF NOT EXISTS tag VARCHAR(100)")
db.Exec("ALTER TABLE stars ADD COLUMN IF NOT EXISTS name_en VARCHAR(500)")
db.Exec("ALTER TABLE stars ADD COLUMN IF NOT EXISTS pic_url VARCHAR(500)")
db.Exec("ALTER TABLE stars ADD COLUMN IF NOT EXISTS description TEXT")
db.Exec("CREATE UNIQUE INDEX IF NOT EXISTS uk_stars_identity_id ON stars(identity_id)")
cleanupServiceTestDB(t, db)
return db
}
// cleanupServiceTestDB 服务层测试数据清理
//
// 沿用 repository 包 cleanupTestDB 的语义:清理 mobile LIKE '199%' 的用户与
// 关联数据,以及 identity_id LIKE 'test_service_%' / 'test_peripheral_%' 的明星。
//
// ★ mint-task-3:增加 crystal_transaction_records(source_id 唯一索引需要),按 source_id
// 含 'reconcile-' / 'rpc-fail-' / 'idem-' 的 order_id 清理,避免跨测试因 unique constraint
// 失败。
func cleanupServiceTestDB(t *testing.T, db *gorm.DB) {
t.Helper()
db.Exec("DELETE FROM crystal_transaction_records WHERE user_id IN (SELECT id FROM users WHERE mobile LIKE '199%') OR source_id LIKE 'rpc-%' OR source_id LIKE 'reconcile-%' OR source_id LIKE 'idem-%'")
db.Exec("DELETE FROM asset_registry WHERE owner_uid IN (SELECT id FROM users WHERE mobile LIKE '199%')")
db.Exec("DELETE FROM peripheral_info WHERE asset_id IN (SELECT id FROM assets WHERE owner_uid IN (SELECT id FROM users WHERE mobile LIKE '199%'))")
db.Exec("DELETE FROM asset_likes WHERE user_id IN (SELECT id FROM users WHERE mobile LIKE '199%') OR asset_id IN (SELECT id FROM assets WHERE owner_uid IN (SELECT id FROM users WHERE mobile LIKE '199%'))")
db.Exec("DELETE FROM assets WHERE owner_uid IN (SELECT id FROM users WHERE mobile LIKE '199%')")
db.Exec("DELETE FROM mint_orders WHERE user_id IN (SELECT id FROM users WHERE mobile LIKE '199%')")
db.Exec("DELETE FROM fan_profiles WHERE user_id IN (SELECT id FROM users WHERE mobile LIKE '199%')")
db.Exec("DELETE FROM users WHERE mobile LIKE '199%'")
db.Exec("DELETE FROM stars WHERE identity_id LIKE 'test_service_%' OR identity_id LIKE 'test_peripheral_%'")
}
// createServiceTestStar 服务层测试用明星创建
func createServiceTestStar(t *testing.T, db *gorm.DB, identityID string) *models.Star {
t.Helper()
var existing models.Star
if err := db.Where("identity_id = ?", identityID).First(&existing).Error; err == nil {
return &existing
}
star := &models.Star{
Name: "测试明星-" + identityID,
IdentityID: identityID,
IsActive: true,
}
if err := db.Create(star).Error; err != nil {
t.Fatalf("Failed to create test star: %v", err)
}
return star
}
// createServiceTestUser 服务层测试用用户创建
func createServiceTestUser(t *testing.T, db *gorm.DB, mobile string) *models.User {
t.Helper()
user := &models.User{
Mobile: mobile,
PasswordHash: "test_hash",
IsActive: true,
}
if err := db.Create(user).Error; err != nil {
t.Fatalf("Failed to create test user: %v", err)
}
return user
}
// createServiceTestAsset 服务层测试用资产创建
func createServiceTestAsset(t *testing.T, db *gorm.DB, ownerUID, starID int64, name string) *models.Asset {
t.Helper()
asset := &models.Asset{
OwnerUID: ownerUID,
StarID: starID,
Name: name,
CoverURL: "http://example.com/p.jpg",
Status: models.AssetStatusActive,
IsActive: true,
}
if err := db.Create(asset).Error; err != nil {
t.Fatalf("Failed to create test asset: %v", err)
}
return asset
}