fix(street): 晋升时重建副图父引用(草稿行 id 到 正式行 id)
This commit is contained in:
@ -38,6 +38,11 @@ type StreetSnapImage struct {
|
|||||||
Phash sql.NullString `gorm:"column:phash;type:vector(64)" json:"phash"`
|
Phash sql.NullString `gorm:"column:phash;type:vector(64)" json:"phash"`
|
||||||
IsDuplicate uint8 `gorm:"column:is_duplicate;not null;default:0" json:"is_duplicate"`
|
IsDuplicate uint8 `gorm:"column:is_duplicate;not null;default:0" json:"is_duplicate"`
|
||||||
DupOf string `gorm:"column:dup_of;type:varchar(32);not null;default:''" json:"dup_of"`
|
DupOf string `gorm:"column:dup_of;type:varchar(32);not null;default:''" json:"dup_of"`
|
||||||
|
|
||||||
|
// 主副图分组(列由 db/migrations/2026-09-21-01-street-main-detail.sql 添加):
|
||||||
|
// is_detail=0 为主图(parent_image_id=0);=1 为副图,parent_image_id 指向同专辑内主图的行 id。
|
||||||
|
IsDetail uint8 `gorm:"column:is_detail;not null;default:0" json:"is_detail"`
|
||||||
|
ParentImageID uint32 `gorm:"column:parent_image_id;not null;default:0" json:"parent_image_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName 指定图片明细表名。
|
// TableName 指定图片明细表名。
|
||||||
|
|||||||
@ -449,22 +449,74 @@ func (r *reviewRepository) SaveStreetSnapFromDraft(ctx context.Context, draftID
|
|||||||
return dErr
|
return dErr
|
||||||
}
|
}
|
||||||
if len(imgs) > 0 {
|
if len(imgs) > 0 {
|
||||||
rows := make([]model.StreetSnapImage, 0, len(imgs))
|
// 第一遍:插入主图(is_detail=0),并记下「草稿行 id → 新正式行 id」的映射。
|
||||||
for i, im := range imgs {
|
//
|
||||||
rows = append(rows, model.StreetSnapImage{
|
// 必须两遍插入:副图的 parent_image_id 存的是**草稿表**的行 id,
|
||||||
SnapID: snapID,
|
// 而这里插入的正式行拿到的是全新 id。若先插副图,父引用就会指向草稿表的旧 id,
|
||||||
Image: im.Image,
|
// 详情页折叠结构随之错乱(见计划「任务 2」标题下的说明)。
|
||||||
Name: im.Name,
|
newIDByDraftID := make(map[uint32]uint32, len(imgs))
|
||||||
SortOrder: uint32(i + 1),
|
mains := make([]model.StreetSnapImage, 0, len(imgs))
|
||||||
Phash: im.Phash,
|
for _, im := range imgs {
|
||||||
IsDuplicate: im.IsDuplicate,
|
if im.IsDetail == 1 {
|
||||||
DupOf: im.DupOf,
|
continue
|
||||||
CreatedAt: now,
|
}
|
||||||
UpdatedAt: now,
|
mains = append(mains, model.StreetSnapImage{
|
||||||
})
|
SnapID: snapID,
|
||||||
|
Image: im.Image,
|
||||||
|
Name: im.Name,
|
||||||
|
SortOrder: uint32(len(mains) + 1),
|
||||||
|
IsDetail: 0,
|
||||||
|
Phash: im.Phash,
|
||||||
|
IsDuplicate: im.IsDuplicate,
|
||||||
|
DupOf: im.DupOf,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if cErr := tx.Create(&rows).Error; cErr != nil {
|
for i := range mains {
|
||||||
return cErr
|
if cErr := tx.Create(&mains[i]).Error; cErr != nil {
|
||||||
|
return cErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mainIdx := 0
|
||||||
|
for _, im := range imgs {
|
||||||
|
if im.IsDetail == 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newIDByDraftID[im.ID] = mains[mainIdx].ID
|
||||||
|
mainIdx++
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第二遍:插入副图,把父引用改写成新主图行 id。
|
||||||
|
details := make([]model.StreetSnapImage, 0, len(imgs)-len(mains))
|
||||||
|
for _, im := range imgs {
|
||||||
|
if im.IsDetail != 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newParent, ok := newIDByDraftID[im.ParentImageID]
|
||||||
|
row := model.StreetSnapImage{
|
||||||
|
SnapID: snapID,
|
||||||
|
Image: im.Image,
|
||||||
|
Name: im.Name,
|
||||||
|
SortOrder: uint32(len(mains) + len(details) + 1),
|
||||||
|
IsDetail: 1,
|
||||||
|
Phash: im.Phash,
|
||||||
|
IsDuplicate: im.IsDuplicate,
|
||||||
|
DupOf: im.DupOf,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
row.ParentImageID = newParent
|
||||||
|
} else {
|
||||||
|
row.IsDetail = 0 // 父行缺失:按主图渲染,与读侧容错口径一致
|
||||||
|
}
|
||||||
|
details = append(details, row)
|
||||||
|
}
|
||||||
|
if len(details) > 0 {
|
||||||
|
if cErr := tx.Create(&details).Error; cErr != nil {
|
||||||
|
return cErr
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -159,3 +159,50 @@ func TestStreetMainDetailReparentGrandchildren(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestStreetPromoteRebuildsParentRef 晋升时必须把副图的父引用从「草稿行 id」改写为「新正式行 id」。
|
||||||
|
func TestStreetPromoteRebuildsParentRef(t *testing.T) {
|
||||||
|
repo := NewReviewRepository(testDB(t))
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
draftID := seedStreetDraft(t, repo)
|
||||||
|
m1 := seedStreetDraftImage(t, repo, draftID, "main", 1)
|
||||||
|
d2 := seedStreetDraftImage(t, repo, draftID, "detail-a", 2)
|
||||||
|
d3 := seedStreetDraftImage(t, repo, draftID, "detail-b", 3)
|
||||||
|
if err := repo.AttachStreetDraftImages(ctx, draftID, m1, []uint32{d2, d3}); err != nil {
|
||||||
|
t.Fatalf("预置主副图失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
snapID, err := repo.SaveStreetSnapFromDraft(ctx, draftID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("晋升失败: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
db := repo.(*reviewRepository).db
|
||||||
|
db.Exec("DELETE FROM street_snap_images WHERE snap_id = ?", snapID)
|
||||||
|
db.Exec("DELETE FROM street_snaps WHERE id = ?", snapID)
|
||||||
|
})
|
||||||
|
|
||||||
|
db := repo.(*reviewRepository).db
|
||||||
|
var rows []model.StreetSnapImage
|
||||||
|
if err := db.WithContext(ctx).Where("snap_id = ? AND is_deleted = 0", snapID).
|
||||||
|
Order("sort_order ASC, id ASC").Find(&rows).Error; err != nil {
|
||||||
|
t.Fatalf("读正式图失败: %v", err)
|
||||||
|
}
|
||||||
|
if len(rows) != 3 {
|
||||||
|
t.Fatalf("期望 3 行正式图,实际 %d", len(rows))
|
||||||
|
}
|
||||||
|
|
||||||
|
mainRow := rows[0]
|
||||||
|
if mainRow.IsDetail != 0 || mainRow.ParentImageID != 0 {
|
||||||
|
t.Fatalf("首行应是主图,实际 is_detail=%d parent=%d", mainRow.IsDetail, mainRow.ParentImageID)
|
||||||
|
}
|
||||||
|
for _, row := range rows[1:] {
|
||||||
|
if row.IsDetail != 1 {
|
||||||
|
t.Fatalf("第 %d 行应是副图", row.ID)
|
||||||
|
}
|
||||||
|
if row.ParentImageID != mainRow.ID {
|
||||||
|
t.Fatalf("副图 parent 应指向新主图行 id %d,实际 %d", mainRow.ID, row.ParentImageID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user