- 数据克隆:安全连接生产环境(42.121.217.194),以只读方式完整克隆 4498 条提号单、30 条财务调整及关联支撑数据到本地数据库并校准 - 回归测试:新增 TestClonedProdDataOperations 端到端自动化回归用例,在 4498 条真实数据上完整验证店铺聚合统计、一键转标准店铺、多变体批量合并清洗、提号列表联动筛选及撤销还原逻辑 - 证据沉淀:更新实施方案文档,登记生产克隆与验证证据;验证通过全套代码门禁 (scripts/check.sh)
203 lines
7.5 KiB
Go
203 lines
7.5 KiB
Go
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("=== 全部克隆数据回归测试用例验证通过! ===")
|
|
}
|