refactor(publish): 审核侧切到正式表,通过即改状态不再晋升
This commit is contained in:
@ -1,6 +1,9 @@
|
||||
//go:build integration
|
||||
|
||||
// 集成测试:街拍草稿主副图分组需要真实 PostgreSQL。
|
||||
// 集成测试:街拍主副图分组需要真实 PostgreSQL。
|
||||
//
|
||||
// 单表发布模型下操作对象是正式表本身(street_snaps / street_snap_images),
|
||||
// 不再有草稿表;审核通过只改状态、不重建图片,故本文件不再覆盖「晋升重建父引用」。
|
||||
//
|
||||
// 运行方式(先起库并确保结构已就绪,见 README「数据库」一节):
|
||||
//
|
||||
@ -13,61 +16,74 @@ package repository
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"fashionapi/internal/model"
|
||||
)
|
||||
|
||||
// seedStreetDraft 插入一条街拍草稿并返回其 id。
|
||||
func seedStreetDraft(t *testing.T, repo ReviewRepository) uint32 {
|
||||
// mainDetailSeedSeq 让每次 seed 出来的实体键(city + year)互不相同。
|
||||
// 正式表上有部分唯一索引 uq_ss_entity (city, year) WHERE is_deleted = 0,
|
||||
// 若测试沿用同一 city/year,第二条记录会被唯一索引拒绝。
|
||||
var mainDetailSeedSeq int
|
||||
|
||||
// seedMainDetailSnap 插入一条街拍正式记录(status=pending)并返回其 id。
|
||||
// 命名加 MainDetail 前缀以避免与 publish_status_integration_test.go 的 seedStreetSnap 冲突。
|
||||
func seedMainDetailSnap(t *testing.T, repo ReviewRepository) uint32 {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
db := repo.(*reviewRepository).db
|
||||
d := model.StreetSnapDraft{Title: "主副图测试", Year: 2026, City: "TestCity", Status: model.DraftStatusPending}
|
||||
if err := db.WithContext(ctx).Create(&d).Error; err != nil {
|
||||
t.Fatalf("插入草稿失败: %v", err)
|
||||
mainDetailSeedSeq++
|
||||
snap := model.StreetSnap{
|
||||
Title: "主副图测试",
|
||||
Year: 2026,
|
||||
City: fmt.Sprintf("TestCity-%d", mainDetailSeedSeq),
|
||||
Status: model.StatusPending,
|
||||
}
|
||||
if err := db.WithContext(ctx).Create(&snap).Error; err != nil {
|
||||
t.Fatalf("插入街拍记录失败: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
db.Exec("DELETE FROM street_snap_draft_images WHERE draft_id = ?", d.ID)
|
||||
db.Exec("DELETE FROM street_snap_drafts WHERE id = ?", d.ID)
|
||||
db.Exec("DELETE FROM street_snap_images WHERE snap_id = ?", snap.ID)
|
||||
db.Exec("DELETE FROM street_snaps WHERE id = ?", snap.ID)
|
||||
})
|
||||
return d.ID
|
||||
return snap.ID
|
||||
}
|
||||
|
||||
// seedStreetDraftImage 插入一张草稿图并返回其行 id。
|
||||
func seedStreetDraftImage(t *testing.T, repo ReviewRepository, draftID uint32, name string, order uint32) uint32 {
|
||||
// seedMainDetailImage 插入一张正式街拍图并返回其行 id。
|
||||
func seedMainDetailImage(t *testing.T, repo ReviewRepository, snapID uint32, name string, order uint32) uint32 {
|
||||
t.Helper()
|
||||
img := model.StreetSnapDraftImage{DraftID: draftID, Image: name + ".jpg", Name: name, SortOrder: order}
|
||||
img := model.StreetSnapImage{SnapID: snapID, Image: name + ".jpg", Name: name, SortOrder: order}
|
||||
if err := repo.(*reviewRepository).db.WithContext(context.Background()).Create(&img).Error; err != nil {
|
||||
t.Fatalf("插入草稿图失败: %v", err)
|
||||
t.Fatalf("插入街拍图失败: %v", err)
|
||||
}
|
||||
return img.ID
|
||||
}
|
||||
|
||||
// reloadStreetImages 取草稿的全部图(按 sort_order, id 升序),便于断言。
|
||||
func reloadStreetImages(t *testing.T, repo ReviewRepository, draftID uint32) []model.StreetSnapDraftImage {
|
||||
// reloadSnapImages 取记录的全部图(按 sort_order, id 升序),便于断言。
|
||||
func reloadSnapImages(t *testing.T, repo ReviewRepository, snapID uint32) []model.StreetSnapImage {
|
||||
t.Helper()
|
||||
imgs, err := repo.StreetDraftImages(context.Background(), draftID)
|
||||
imgs, err := repo.StreetRecordImages(context.Background(), snapID)
|
||||
if err != nil {
|
||||
t.Fatalf("读草稿图失败: %v", err)
|
||||
t.Fatalf("读街拍图失败: %v", err)
|
||||
}
|
||||
return imgs
|
||||
}
|
||||
|
||||
// TestStreetMainDetail 覆盖批量并入 / 并入上一张 / 拆出 / 跨草稿拦截四条主线。
|
||||
// TestStreetMainDetail 覆盖批量并入 / 拆出 / 跨记录拦截三条主线。
|
||||
func TestStreetMainDetail(t *testing.T) {
|
||||
repo := NewReviewRepository(testDB(t))
|
||||
ctx := context.Background()
|
||||
|
||||
draftID := seedStreetDraft(t, repo)
|
||||
m1 := seedStreetDraftImage(t, repo, draftID, "a", 1)
|
||||
d2 := seedStreetDraftImage(t, repo, draftID, "b", 2)
|
||||
d3 := seedStreetDraftImage(t, repo, draftID, "c", 3)
|
||||
snapID := seedMainDetailSnap(t, repo)
|
||||
m1 := seedMainDetailImage(t, repo, snapID, "a", 1)
|
||||
d2 := seedMainDetailImage(t, repo, snapID, "b", 2)
|
||||
d3 := seedMainDetailImage(t, repo, snapID, "c", 3)
|
||||
|
||||
if err := repo.AttachStreetDraftImages(ctx, draftID, m1, []uint32{d2, d3}); err != nil {
|
||||
if err := repo.AttachStreetSnapImages(ctx, snapID, m1, []uint32{d2, d3}); err != nil {
|
||||
t.Fatalf("并入失败: %v", err)
|
||||
}
|
||||
for _, im := range reloadStreetImages(t, repo, draftID) {
|
||||
for _, im := range reloadSnapImages(t, repo, snapID) {
|
||||
switch im.ID {
|
||||
case m1:
|
||||
if im.IsDetail != 0 || im.ParentImageID != 0 {
|
||||
@ -81,40 +97,40 @@ func TestStreetMainDetail(t *testing.T) {
|
||||
}
|
||||
|
||||
// 幂等:重复并入同一批不报错,结果不变。
|
||||
if err := repo.AttachStreetDraftImages(ctx, draftID, m1, []uint32{d2, d3}); err != nil {
|
||||
if err := repo.AttachStreetSnapImages(ctx, snapID, m1, []uint32{d2, d3}); err != nil {
|
||||
t.Fatalf("重复并入应幂等,实际报错: %v", err)
|
||||
}
|
||||
|
||||
// 传入主图自身:调用方过滤掉的语义由仓储兜住(id <> main 条件),不报错也不改动主图。
|
||||
if err := repo.AttachStreetDraftImages(ctx, draftID, m1, []uint32{m1}); err != nil {
|
||||
if err := repo.AttachStreetSnapImages(ctx, snapID, m1, []uint32{m1}); err != nil {
|
||||
t.Fatalf("并入集合含主图自身应被忽略而非报错: %v", err)
|
||||
}
|
||||
for _, im := range reloadStreetImages(t, repo, draftID) {
|
||||
for _, im := range reloadSnapImages(t, repo, snapID) {
|
||||
if im.ID == m1 && (im.IsDetail != 0 || im.ParentImageID != 0) {
|
||||
t.Fatalf("主图被自身并入后不应变化")
|
||||
}
|
||||
}
|
||||
|
||||
// 拆出:把 d3 恢复为主图。
|
||||
if err := repo.DetachStreetDraftImage(ctx, draftID, d3); err != nil {
|
||||
if err := repo.DetachStreetSnapImage(ctx, snapID, d3); err != nil {
|
||||
t.Fatalf("拆出失败: %v", err)
|
||||
}
|
||||
for _, im := range reloadStreetImages(t, repo, draftID) {
|
||||
for _, im := range reloadSnapImages(t, repo, snapID) {
|
||||
if im.ID == d3 && (im.IsDetail != 0 || im.ParentImageID != 0) {
|
||||
t.Fatalf("拆出后应为 is_detail=0/parent=0,实际 %d/%d", im.IsDetail, im.ParentImageID)
|
||||
}
|
||||
}
|
||||
if err := repo.DetachStreetDraftImage(ctx, draftID, m1); err != nil { // 对主图幂等成功
|
||||
if err := repo.DetachStreetSnapImage(ctx, snapID, m1); err != nil { // 对主图幂等成功
|
||||
t.Fatalf("对主图拆出应幂等成功: %v", err)
|
||||
}
|
||||
|
||||
// 跨草稿拦截:空集合 + 跨草稿主图。
|
||||
if err := repo.AttachStreetDraftImages(ctx, draftID, m1, nil); !errors.Is(err, ErrInvalidImageGroup) {
|
||||
// 跨记录拦截:空集合 + 跨记录主图。
|
||||
if err := repo.AttachStreetSnapImages(ctx, snapID, m1, nil); !errors.Is(err, ErrInvalidImageGroup) {
|
||||
t.Fatalf("空集合应返回 ErrInvalidImageGroup,实际 %v", err)
|
||||
}
|
||||
otherMain := seedStreetDraftImage(t, repo, seedStreetDraft(t, repo), "x", 1)
|
||||
if err := repo.AttachStreetDraftImages(ctx, draftID, otherMain, []uint32{d2}); !errors.Is(err, ErrInvalidImageGroup) {
|
||||
t.Fatalf("跨草稿主图应被拦截,实际 %v", err)
|
||||
otherMain := seedMainDetailImage(t, repo, seedMainDetailSnap(t, repo), "x", 1)
|
||||
if err := repo.AttachStreetSnapImages(ctx, snapID, otherMain, []uint32{d2}); !errors.Is(err, ErrInvalidImageGroup) {
|
||||
t.Fatalf("跨记录主图应被拦截,实际 %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,18 +139,18 @@ func TestStreetMainDetailReparentGrandchildren(t *testing.T) {
|
||||
repo := NewReviewRepository(testDB(t))
|
||||
ctx := context.Background()
|
||||
|
||||
draftID := seedStreetDraft(t, repo)
|
||||
m1 := seedStreetDraftImage(t, repo, draftID, "m1", 1)
|
||||
m2 := seedStreetDraftImage(t, repo, draftID, "m2", 2)
|
||||
child := seedStreetDraftImage(t, repo, draftID, "child", 3)
|
||||
snapID := seedMainDetailSnap(t, repo)
|
||||
m1 := seedMainDetailImage(t, repo, snapID, "m1", 1)
|
||||
m2 := seedMainDetailImage(t, repo, snapID, "m2", 2)
|
||||
child := seedMainDetailImage(t, repo, snapID, "child", 3)
|
||||
|
||||
if err := repo.AttachStreetDraftImages(ctx, draftID, m2, []uint32{child}); err != nil {
|
||||
if err := repo.AttachStreetSnapImages(ctx, snapID, m2, []uint32{child}); err != nil {
|
||||
t.Fatalf("预置副图失败: %v", err)
|
||||
}
|
||||
if err := repo.AttachStreetDraftImages(ctx, draftID, m1, []uint32{m2}); err != nil {
|
||||
if err := repo.AttachStreetSnapImages(ctx, snapID, m1, []uint32{m2}); err != nil {
|
||||
t.Fatalf("并入整组失败: %v", err)
|
||||
}
|
||||
for _, im := range reloadStreetImages(t, repo, draftID) {
|
||||
for _, im := range reloadSnapImages(t, repo, snapID) {
|
||||
if im.ID == m2 && (im.IsDetail != 1 || im.ParentImageID != m1) {
|
||||
t.Fatalf("m2 应成为 m1 的副图,实际 %d/%d", im.IsDetail, im.ParentImageID)
|
||||
}
|
||||
@ -143,50 +159,3 @@ 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