package pickup import ( "context" "testing" "time" "hfb_sys/backend/internal/auditlog" "hfb_sys/backend/internal/model" "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/logger" ) func TestClonedProdDataOperations(t *testing.T) { dsn := "hfb:secret@tcp(127.0.0.1:13306)/hfb_sys?charset=utf8mb4&parseTime=True&loc=Local" db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { t.Skipf("无法连接本地 hfb_sys (可能在CI环境或容器未开启): %v", err) } var pickupCount int64 if err := db.Model(&model.AdminPickup{}).Count(&pickupCount).Error; err != nil || pickupCount < 1000 { t.Skipf("本地 hfb_sys 未包含生产克隆数据 (当前 count=%d, err=%v),跳过克隆数据回归测试", pickupCount, err) } repo := NewRepository(db) ctx := context.Background() t.Logf("=== 1. 验证克隆生产环境数据基础特征 (单量: %d) ===", pickupCount) if pickupCount != 4498 { t.Logf("注意: 当前提号记录数为 %d (线上基准 4498)", pickupCount) } // 为保证测试幂等性,先将测试用的几个变体恢复至原始未映射状态 db.Exec("DELETE FROM pickup_shop_mappings WHERE source_name IN ('王大锤游戏专营', '淘宝王大锤商行', '王大锤商行')") db.Exec("UPDATE admin_pickups SET shop_name = raw_shop_name WHERE raw_shop_name IN ('王大锤游戏专营', '淘宝王大锤商行', '王大锤商行')") // 1. 获取初始历史聚合统计 stats, err := repo.GetHistoricalShopStats(ctx) if err != nil { t.Fatalf("GetHistoricalShopStats() 失败: %v", err) } t.Logf("统计概览: 总店铺数=%d, 涉及历史提号总单量=%d, 标准店=%d, 已映射=%d, 待映射=%d", stats.TotalShops, stats.TotalPickups, stats.StandardCount, stats.MappedCount, stats.UnmappedCount) if stats.TotalPickups != pickupCount { t.Fatalf("统计单量与数据库记录数不一致: 统计=%d, 表总数=%d", stats.TotalPickups, pickupCount) } if stats.TotalShops < 60 { t.Fatalf("店铺种类数与线上统计不符: %d (预期 >= 60)", stats.TotalShops) } // 打印 Top 5 店铺 for i := 0; i < 5 && i < len(stats.Items); i++ { t.Logf(" Top %d: 「%s」 单量=%d, 状态=%s", i+1, stats.Items[i].SourceName, stats.Items[i].Count, stats.Items[i].MappingStatus) } // 2. 测试一键将「糖浆」设为标准店铺 t.Log("=== 2. 测试一键收录标准店铺 「糖浆」 ===") // 先检查是否已有 var existingTangjiang model.PickupShop if err := db.Where("name = ?", "糖浆").First(&existingTangjiang).Error; err == gorm.ErrRecordNotFound { _, err := repo.CreateShop(ctx, CreatePickupShopRequest{ Name: "糖浆", Platform: "拼多多", Status: PickupShopStatusActive, SortOrder: 100, Remark: "测试收录", }, 1, auditlog.Meta{}) if err != nil { t.Fatalf("收录「糖浆」失败: %v", err) } } // 重新统计,验证「糖浆」状态变为 standard statsAfterTangjiang, err := repo.GetHistoricalShopStats(ctx) if err != nil { t.Fatalf("重新获取统计失败: %v", err) } var tangjiangItem *PickupShopHistoricalItemDTO for _, it := range statsAfterTangjiang.Items { if it.SourceName == "糖浆" { tangjiangItem = &it break } } if tangjiangItem == nil || tangjiangItem.MappingStatus != "standard" { t.Fatalf("「糖浆」状态未变为 standard: %+v", tangjiangItem) } t.Logf("✓ 「糖浆」成功识别为标准店铺 (历史单量: %d)", tangjiangItem.Count) // 3. 测试批量映射与数据清洗 t.Log("=== 3. 测试批量映射变体至标准店铺 「王大锤游戏」 ===") // 先确保目标标准店铺「王大锤游戏」存在 var stdDachui model.PickupShop if err := db.Where("name = ?", "王大锤游戏").First(&stdDachui).Error; err == gorm.ErrRecordNotFound { createdDachui, err := repo.CreateShop(ctx, CreatePickupShopRequest{ Name: "王大锤游戏", Platform: "淘宝", Status: PickupShopStatusActive, }, 1, auditlog.Meta{}) if err != nil { t.Fatalf("创建「王大锤游戏」失败: %v", err) } stdDachui = model.PickupShop{ID: createdDachui.ID, Name: createdDachui.Name} } // 待合并清洗变体:王大锤游戏专营 (36单), 淘宝王大锤商行 (3单), 王大锤商行 (1单) variants := []string{"王大锤游戏专营", "淘宝王大锤商行", "王大锤商行"} mapRes, err := repo.MapHistoricalShops(ctx, MapHistoricalShopsRequest{ FromShopNames: variants, TargetShopName: "王大锤游戏", Remark: "自动化回归批量合并测试", }, 1, auditlog.Meta{}) if err != nil { t.Fatalf("MapHistoricalShops() 失败: %v", err) } t.Logf("✓ 批量映射成功: 清洗更新提号单数 = %d, 目标店铺 = %s (ID=%d)", mapRes.UpdatedPickupsCount, mapRes.TargetShopName, mapRes.TargetShopID) if mapRes.UpdatedPickupsCount != 40 { t.Fatalf("预期清洗 40 单 (36+3+1), 实际清洗 = %d", mapRes.UpdatedPickupsCount) } // 4. 验证数据库中这 40 单的 shop_name 和 raw_shop_name t.Log("=== 4. 验证提号单持久化字段状态 ===") var checkRows []struct { ID uint64 ShopName string RawShopName string } if err := db.Model(&model.AdminPickup{}). Where("raw_shop_name IN ?", variants). Scan(&checkRows).Error; err != nil { t.Fatalf("查询已清洗记录失败: %v", err) } if len(checkRows) != 40 { t.Fatalf("查询到已清洗记录数 = %d, want 40", len(checkRows)) } for _, r := range checkRows { if r.ShopName != "王大锤游戏" { t.Fatalf("ID=%d shop_name 未更新为目标: %q", r.ID, r.ShopName) } if r.RawShopName == "" || r.RawShopName == "王大锤游戏" { t.Fatalf("ID=%d raw_shop_name 丢失了原始字样: %q", r.ID, r.RawShopName) } } t.Log("✓ 全部 40 单提号 shop_name 已清洗为「王大锤游戏」,且 raw_shop_name 完美保留原始变体字样!") // 5. 验证后台列表查询与店铺聚合过滤 t.Log("=== 5. 验证后台提号列表筛选 ===") listRes, err := repo.ListAdmin(ctx, AdminPickupQuery{ ShopName: "王大锤游戏", Page: 1, PageSize: 20, }) if err != nil { t.Fatalf("按「王大锤游戏」筛选列表失败: %v", err) } t.Logf("✓ 按「王大锤游戏」筛选总单量: %d", listRes.Total) if listRes.Total < 880 { t.Fatalf("合并后「王大锤游戏」总单量预期 >= 880, 实际: %d", listRes.Total) } // 验证时间范围筛选 startTime, _ := time.Parse("2006-01-02", "2026-08-01") endTime, _ := time.Parse("2006-01-02", "2026-08-31") timeRes, err := repo.ListAdmin(ctx, AdminPickupQuery{ StartTime: &startTime, EndTime: &endTime, Page: 1, PageSize: 20, }) if err != nil { t.Fatalf("时间范围筛选失败: %v", err) } t.Logf("✓ 2026年8月提号单量: %d", timeRes.Total) if timeRes.Total == 0 { t.Fatal("2026年8月提号单量不应为 0") } // 6. 测试撤销映射并还原数据 t.Log("=== 6. 测试撤销映射规则并还原提号单 ===") if err := repo.DeleteShopMapping(ctx, "王大锤商行", 1, auditlog.Meta{}); err != nil { t.Fatalf("撤销「王大锤商行」映射失败: %v", err) } var restoredRow model.AdminPickup if err := db.Where("raw_shop_name = ?", "王大锤商行").First(&restoredRow).Error; err != nil { t.Fatalf("查询还原记录失败: %v", err) } if restoredRow.ShopName != "王大锤商行" { t.Fatalf("撤销后 shop_name 未还原为原始值: 实际 %q, 期望 %q", restoredRow.ShopName, "王大锤商行") } t.Logf("✓ 「王大锤商行」映射已成功撤销,原提号单 shop_name 已安全还原为「王大锤商行」!") t.Log("=== 全部克隆数据回归测试用例验证通过! ===") }