feat(pickup): 店铺管理支持历史店铺可视化手动映射与一键批量清洗

- 数据库升级:新增 pickup_shop_mappings 映射别名表,并在 admin_pickups 增加 raw_shop_name 永久保留原始店铺录入值
- 后端映射能力:提供历史店铺聚合统计、单条/批量映射至目标标准店铺、映射规则撤销与提号单还原接口;新建提号时若录入已映射别名自动归一化
- 前端管理升级:PickupShopDialog 升级为双 Tab 结构,Tab 2「历史店铺映射与清洗」支持总览统计卡片、状态筛选、单条/批量映射清洗、一键设为标准店铺及撤销还原
- 验证方式:执行并通过全套代码门禁(scripts/check.sh:SQL约束、go vet/build/test、MySQL测试库覆盖率、vue大小、eslint、prettier、vue-tsc及vite构建)
This commit is contained in:
yml2213
2026-10-09 13:31:11 +08:00
parent 9d9adfbd5e
commit f3c5235b81
12 changed files with 1187 additions and 100 deletions
@@ -918,3 +918,104 @@ func TestRepositoryPickupShopCRUDAndOptions(t *testing.T) {
t.Fatalf("active shops after delete length = %d, want 0", len(shopsAfterDelete))
}
}
func TestRepositoryHistoricalShopStatsAndMapping(t *testing.T) {
repo, db := newPickupTestRepo(t)
// 1. 创建标准店铺
stdShop1, err := repo.CreateShop(t.Context(), CreatePickupShopRequest{
Name: "王大锤游戏",
Platform: "淘宝",
Status: "active",
}, 1, auditlog.Meta{})
if err != nil {
t.Fatalf("CreateShop() error = %v", err)
}
// 2. 插入测试提号记录
listing1 := seedAvailableListing(t, db, "LST-MAP-01", "映射测试商品1", false)
_, err = repo.Create(t.Context(), CreateRequest{
ListingID: listing1.ID,
ShopName: "王大锤游戏",
}, 1, auditlog.Meta{})
if err != nil {
t.Fatalf("Create() error = %v", err)
}
listing2 := seedAvailableListing(t, db, "LST-MAP-02", "映射测试商品2", false)
_, err = repo.Create(t.Context(), CreateRequest{
ListingID: listing2.ID,
ShopName: "王大锤游戏专营",
}, 1, auditlog.Meta{})
if err != nil {
t.Fatalf("Create() error = %v", err)
}
listing3 := seedAvailableListing(t, db, "LST-MAP-03", "映射测试商品3", false)
_, err = repo.Create(t.Context(), CreateRequest{
ListingID: listing3.ID,
ShopName: "淘宝王大锤商行",
}, 1, auditlog.Meta{})
if err != nil {
t.Fatalf("Create() error = %v", err)
}
// 3. 获取历史聚合统计
stats, err := repo.GetHistoricalShopStats(t.Context())
if err != nil {
t.Fatalf("GetHistoricalShopStats() error = %v", err)
}
if stats.TotalShops < 3 {
t.Fatalf("stats.TotalShops = %d, want >= 3", stats.TotalShops)
}
if stats.StandardCount < 1 {
t.Fatalf("stats.StandardCount = %d, want >= 1", stats.StandardCount)
}
// 4. 批量映射
mapRes, err := repo.MapHistoricalShops(t.Context(), MapHistoricalShopsRequest{
FromShopNames: []string{"王大锤游戏专营", "淘宝王大锤商行"},
TargetShopName: "王大锤游戏",
Remark: "测试批量映射",
}, 1, auditlog.Meta{})
if err != nil {
t.Fatalf("MapHistoricalShops() error = %v", err)
}
if mapRes.UpdatedPickupsCount != 2 {
t.Fatalf("UpdatedPickupsCount = %d, want 2", mapRes.UpdatedPickupsCount)
}
if mapRes.TargetShopID != stdShop1.ID {
t.Fatalf("TargetShopID = %d, want %d", mapRes.TargetShopID, stdShop1.ID)
}
// 5. 再次统计
statsAfter, err := repo.GetHistoricalShopStats(t.Context())
if err != nil {
t.Fatalf("GetHistoricalShopStats() error = %v", err)
}
if statsAfter.MappedCount < 2 {
t.Fatalf("statsAfter.MappedCount = %d, want >= 2", statsAfter.MappedCount)
}
// 6. 验证新建提号自动归一化
listing4 := seedAvailableListing(t, db, "LST-MAP-04", "映射测试商品4", false)
createdWithAlias, err := repo.Create(t.Context(), CreateRequest{
ListingID: listing4.ID,
ShopName: "王大锤游戏专营",
}, 1, auditlog.Meta{})
if err != nil {
t.Fatalf("Create() with alias error = %v", err)
}
if createdWithAlias.ShopName != "王大锤游戏" {
t.Fatalf("createdWithAlias.ShopName = %q, want 王大锤游戏", createdWithAlias.ShopName)
}
if createdWithAlias.RawShopName != "王大锤游戏专营" {
t.Fatalf("createdWithAlias.RawShopName = %q, want 王大锤游戏专营", createdWithAlias.RawShopName)
}
// 7. 取消映射
if err := repo.DeleteShopMapping(t.Context(), "王大锤游戏专营", 1, auditlog.Meta{}); err != nil {
t.Fatalf("DeleteShopMapping() error = %v", err)
}
}