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"`
|
||||
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"`
|
||||
|
||||
// 主副图分组(列由 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 指定图片明细表名。
|
||||
|
||||
@ -449,13 +449,23 @@ func (r *reviewRepository) SaveStreetSnapFromDraft(ctx context.Context, draftID
|
||||
return dErr
|
||||
}
|
||||
if len(imgs) > 0 {
|
||||
rows := make([]model.StreetSnapImage, 0, len(imgs))
|
||||
for i, im := range imgs {
|
||||
rows = append(rows, model.StreetSnapImage{
|
||||
// 第一遍:插入主图(is_detail=0),并记下「草稿行 id → 新正式行 id」的映射。
|
||||
//
|
||||
// 必须两遍插入:副图的 parent_image_id 存的是**草稿表**的行 id,
|
||||
// 而这里插入的正式行拿到的是全新 id。若先插副图,父引用就会指向草稿表的旧 id,
|
||||
// 详情页折叠结构随之错乱(见计划「任务 2」标题下的说明)。
|
||||
newIDByDraftID := make(map[uint32]uint32, len(imgs))
|
||||
mains := make([]model.StreetSnapImage, 0, len(imgs))
|
||||
for _, im := range imgs {
|
||||
if im.IsDetail == 1 {
|
||||
continue
|
||||
}
|
||||
mains = append(mains, model.StreetSnapImage{
|
||||
SnapID: snapID,
|
||||
Image: im.Image,
|
||||
Name: im.Name,
|
||||
SortOrder: uint32(i + 1),
|
||||
SortOrder: uint32(len(mains) + 1),
|
||||
IsDetail: 0,
|
||||
Phash: im.Phash,
|
||||
IsDuplicate: im.IsDuplicate,
|
||||
DupOf: im.DupOf,
|
||||
@ -463,10 +473,52 @@ func (r *reviewRepository) SaveStreetSnapFromDraft(ctx context.Context, draftID
|
||||
UpdatedAt: now,
|
||||
})
|
||||
}
|
||||
if cErr := tx.Create(&rows).Error; cErr != nil {
|
||||
for i := range mains {
|
||||
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
|
||||
})
|
||||
if err != 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