- 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>
134 lines
5.1 KiB
Go
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
|
|
}
|