Files
junhong_cmp_fiber/tests/testutils/db.go
huang 1cf17e8f14
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m46s
清理冗余的梯度返佣(TierCommission)配置
- 移除 Model 层:删除 ShopSeriesCommissionTier 模型及相关字段
- 更新 DTO:删除 TierCommissionConfig、TierEntry 类型及相关请求/响应字段
- 删除 Store 层:移除 ShopSeriesCommissionTierStore 及相关查询逻辑
- 简化 Service 层:删除梯度返佣处理逻辑,统计查询移除 tier_bonus 字段
- 数据库迁移:创建 000034_remove_tier_commission 移除相关表和字段
- 更新测试:移除梯度返佣相关测试用例,更新集成测试
- OpenAPI 文档:删除梯度返佣相关 schema 和枚举值
- 归档变更:归档 remove-tier-commission-redundancy 到 archive/2026-01-30-
- 同步规范:更新 4 个主 specs,标记废弃功能并添加迁移指引

原因:梯度返佣功能与一次性梯度佣金功能重复,且从未实现实际计算逻辑
迁移:使用一次性佣金的梯度模式 (OneTimeCommissionConfig.type = "tiered") 替代
2026-01-30 14:57:24 +08:00

248 lines
6.4 KiB
Go

package testutils
import (
"context"
"fmt"
"strings"
"sync"
"testing"
"github.com/redis/go-redis/v9"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"github.com/break/junhong_cmp_fiber/internal/model"
)
// 全局单例数据库和 Redis 连接
// 使用 sync.Once 确保整个测试套件只创建一次连接,显著提升测试性能
var (
testDBOnce sync.Once
testDB *gorm.DB
testDBInitErr error
testRedisOnce sync.Once
testRedis *redis.Client
testRedisInitErr error
)
// 测试数据库配置
// TODO: 未来可以从环境变量或配置文件加载
const (
testDBDSN = "host=cxd.whcxd.cn port=16159 user=erp_pgsql password=erp_2025 dbname=junhong_cmp_test sslmode=disable TimeZone=Asia/Shanghai"
testRedisAddr = "cxd.whcxd.cn:16299"
testRedisPasswd = "cpNbWtAaqgo1YJmbMp3h"
testRedisDB = 15
)
// GetTestDB 获取全局单例测试数据库连接
//
// 特点:
// - 使用 sync.Once 确保整个测试套件只创建一次连接
// - AutoMigrate 只在首次连接时执行一次
// - 连接失败会跳过测试(不是致命错误)
//
// 用法:
//
// func TestXxx(t *testing.T) {
// db := testutils.GetTestDB(t)
// // db 是全局共享的连接,不要直接修改其状态
// // 如需事务隔离,使用 NewTestTransaction(t)
// }
func GetTestDB(t *testing.T) *gorm.DB {
t.Helper()
testDBOnce.Do(func() {
var err error
testDB, err = gorm.Open(postgres.Open(testDBDSN), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
testDBInitErr = fmt.Errorf("无法连接测试数据库: %w", err)
return
}
err = testDB.AutoMigrate(
&model.Account{},
&model.Role{},
&model.Permission{},
&model.AccountRole{},
&model.RolePermission{},
&model.Shop{},
&model.Enterprise{},
&model.PersonalCustomer{},
&model.PersonalCustomerPhone{},
&model.PersonalCustomerICCID{},
&model.PersonalCustomerDevice{},
&model.IotCard{},
&model.IotCardImportTask{},
&model.Device{},
&model.DeviceImportTask{},
&model.DeviceSimBinding{},
&model.Carrier{},
&model.Tag{},
&model.PackageSeries{},
&model.Package{},
&model.ShopPackageAllocation{},
&model.ShopSeriesAllocation{},
&model.EnterpriseCardAuthorization{},
&model.EnterpriseDeviceAuthorization{},
&model.AssetAllocationRecord{},
&model.CommissionWithdrawalRequest{},
&model.CommissionWithdrawalSetting{},
)
if err != nil {
errMsg := err.Error()
if strings.Contains(errMsg, "does not exist") && strings.Contains(errMsg, "constraint") {
// 忽略约束不存在的错误,这是由于约束名变更导致的
} else {
testDBInitErr = fmt.Errorf("数据库迁移失败: %w", err)
return
}
}
})
if testDBInitErr != nil {
t.Skipf("跳过测试:%v", testDBInitErr)
}
return testDB
}
// GetTestRedis 获取全局单例 Redis 连接
//
// 特点:
// - 使用 sync.Once 确保整个测试套件只创建一次连接
// - 连接失败会跳过测试(不是致命错误)
//
// 用法:
//
// func TestXxx(t *testing.T) {
// rdb := testutils.GetTestRedis(t)
// // rdb 是全局共享的连接
// // 使用 CleanTestRedisKeys(t) 自动清理测试相关的 Redis 键
// }
func GetTestRedis(t *testing.T) *redis.Client {
t.Helper()
testRedisOnce.Do(func() {
testRedis = redis.NewClient(&redis.Options{
Addr: testRedisAddr,
Password: testRedisPasswd,
DB: testRedisDB,
})
ctx := context.Background()
if err := testRedis.Ping(ctx).Err(); err != nil {
testRedisInitErr = fmt.Errorf("无法连接 Redis: %w", err)
return
}
})
if testRedisInitErr != nil {
t.Skipf("跳过测试:%v", testRedisInitErr)
}
return testRedis
}
// NewTestTransaction 创建测试事务,自动在测试结束时回滚
//
// 特点:
// - 每个测试用例获得独立的事务,互不干扰
// - 使用 t.Cleanup() 确保即使测试 panic 也能回滚
// - 回滚后数据库状态与测试前完全一致
//
// 用法:
//
// func TestXxx(t *testing.T) {
// tx := testutils.NewTestTransaction(t)
// // 所有数据库操作使用 tx 而非 db
// store := postgres.NewXxxStore(tx, rdb)
// // 测试结束后自动回滚,无需手动清理
// }
//
// 注意:
// - 不要在子测试(t.Run)中调用此函数,因为子测试可能并行执行
// - 如需在子测试中使用数据库,应在父测试中创建事务并传递
func NewTestTransaction(t *testing.T) *gorm.DB {
t.Helper()
db := GetTestDB(t)
tx := db.Begin()
if tx.Error != nil {
t.Fatalf("开启测试事务失败: %v", tx.Error)
}
// 使用 t.Cleanup() 确保测试结束时自动回滚
// 即使测试 panic 也能执行清理
t.Cleanup(func() {
tx.Rollback()
})
return tx
}
// CleanTestRedisKeys 清理当前测试的 Redis 键
//
// 特点:
// - 使用测试名称作为键前缀,格式: test:{TestName}:*
// - 测试开始时清理已有键(防止脏数据)
// - 使用 t.Cleanup() 确保测试结束时自动清理
//
// 用法:
//
// func TestXxx(t *testing.T) {
// rdb := testutils.GetTestRedis(t)
// testutils.CleanTestRedisKeys(t, rdb)
// // Redis 键使用测试专用前缀: test:TestXxx:your_key
// }
//
// 键命名规范:
// - 测试中创建的键应使用 GetTestRedisKeyPrefix(t) 作为前缀
// - 例如: test:TestShopStore_Create:cache:shop:1
func CleanTestRedisKeys(t *testing.T, rdb *redis.Client) {
t.Helper()
ctx := context.Background()
testPrefix := GetTestRedisKeyPrefix(t)
// 测试开始前清理已有键
cleanKeys(ctx, rdb, testPrefix)
// 测试结束时自动清理
t.Cleanup(func() {
cleanKeys(ctx, rdb, testPrefix)
})
}
// GetTestRedisKeyPrefix 获取当前测试的 Redis 键前缀
//
// 返回格式: test:{TestName}:
// 用于在测试中创建带前缀的 Redis 键,确保键不会与其他测试冲突
//
// 用法:
//
// func TestXxx(t *testing.T) {
// prefix := testutils.GetTestRedisKeyPrefix(t)
// key := prefix + "my_cache_key"
// // key = "test:TestXxx:my_cache_key"
// }
func GetTestRedisKeyPrefix(t *testing.T) string {
t.Helper()
return fmt.Sprintf("test:%s:", t.Name())
}
// cleanKeys 清理匹配前缀的所有 Redis 键
func cleanKeys(ctx context.Context, rdb *redis.Client, prefix string) {
keys, err := rdb.Keys(ctx, prefix+"*").Result()
if err != nil {
// 忽略 Redis 错误,不影响测试
return
}
if len(keys) > 0 {
rdb.Del(ctx, keys...)
}
}