feat(street): 后台审核页街拍主副图分组渲染与操作路由
This commit is contained in:
@ -94,6 +94,10 @@ func NewBackstage(opt BackstageOptions) *gin.Engine {
|
||||
admin.POST("/reviews/:kind/:id/approve", opt.Backstage.ReviewApprove)
|
||||
admin.POST("/reviews/:kind/:id/reject", opt.Backstage.ReviewReject)
|
||||
admin.POST("/reviews/:kind/:id/images/:img/delete", opt.Backstage.ReviewDraftImageDelete)
|
||||
// 街拍主副图分组操作(零 JS:同步表单 POST,成功 302 回详情页)。
|
||||
admin.POST("/reviews/:kind/:id/images/attach", opt.Backstage.ReviewAttachDraftImages)
|
||||
admin.POST("/reviews/:kind/:id/images/:img/attach-prev", opt.Backstage.ReviewAttachPrevDraftImage)
|
||||
admin.POST("/reviews/:kind/:id/images/:img/detach", opt.Backstage.ReviewDetachDraftImage)
|
||||
// 爬虫入库任务监控:列出处理进度 + 失败重试
|
||||
admin.GET("/ingest-jobs", opt.Backstage.IngestJobs)
|
||||
admin.POST("/ingest-jobs/:id/retry", opt.Backstage.IngestJobRetry)
|
||||
|
||||
@ -14,6 +14,8 @@ import (
|
||||
"fashionapi/internal/model"
|
||||
"fashionapi/internal/pkg/jwt"
|
||||
"fashionapi/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ── 测试用假实现(不连库)──────────────────────────────────────────────
|
||||
@ -175,7 +177,7 @@ func TestBackstage(t *testing.T) {
|
||||
m := jwt.NewManager("test-secret", 168)
|
||||
auth := &fakeAuth{jwt: m}
|
||||
brand := &fakeBrand{}
|
||||
h := handler.NewBackstageHandler(auth, brand, &fakeArticle{}, &fakeStreet{}, nil, nil)
|
||||
h := handler.NewBackstageHandler(auth, brand, &fakeArticle{}, &fakeStreet{}, &fakeReview{attachOK: true}, nil)
|
||||
r := NewBackstage(BackstageOptions{Config: &config.Config{}, JWT: m, Backstage: h})
|
||||
|
||||
// 1) 登录页可访问
|
||||
@ -288,3 +290,166 @@ func TestBackstage(t *testing.T) {
|
||||
t.Fatalf("街拍详情异常: %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// ── 街拍主副图分组:路由 / 重定向 / 渲染(无需真实数据库)─────────────────
|
||||
|
||||
// fakeReview 实现审核服务接口;嵌入接口兜底未用到的方法,仅覆盖分组相关 4 个。
|
||||
type fakeReview struct {
|
||||
service.ReviewService
|
||||
attachOK bool
|
||||
last struct {
|
||||
kind string
|
||||
draftID uint32
|
||||
mainID uint32
|
||||
imgIDs []uint32
|
||||
imgID uint32
|
||||
op string
|
||||
}
|
||||
view *service.DraftDetailView
|
||||
}
|
||||
|
||||
var _ service.ReviewService = (*fakeReview)(nil)
|
||||
|
||||
func (f *fakeReview) DraftDetail(_ context.Context, _ string, _ uint32) (*service.DraftDetailView, error) {
|
||||
if f.view != nil {
|
||||
return f.view, nil
|
||||
}
|
||||
return &service.DraftDetailView{Kind: "street", ID: 7, Title: "T", Year: 2026, City: "Beijing", ImageCount: 2, Status: "pending"}, nil
|
||||
}
|
||||
|
||||
func (f *fakeReview) AttachDraftImages(_ context.Context, kind string, draftID, mainID uint32, imgIDs []uint32) error {
|
||||
f.last.kind, f.last.draftID, f.last.mainID, f.last.imgIDs, f.last.op = kind, draftID, mainID, imgIDs, "attach"
|
||||
if !f.attachOK {
|
||||
return errors.New("主图无效")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (f *fakeReview) AttachPrevDraftImage(_ context.Context, kind string, draftID, imgID uint32) error {
|
||||
f.last.kind, f.last.draftID, f.last.imgID, f.last.op = kind, draftID, imgID, "attach-prev"
|
||||
if !f.attachOK {
|
||||
return errors.New("主图无效")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (f *fakeReview) DetachDraftImage(_ context.Context, kind string, draftID, imgID uint32) error {
|
||||
f.last.kind, f.last.draftID, f.last.imgID, f.last.op = kind, draftID, imgID, "detach"
|
||||
if !f.attachOK {
|
||||
return errors.New("主图无效")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newReviewRouter(t *testing.T) (*gin.Engine, *fakeReview) {
|
||||
t.Helper()
|
||||
m := jwt.NewManager("test-secret", 168)
|
||||
auth := &fakeAuth{jwt: m}
|
||||
brand := &fakeBrand{}
|
||||
rev := &fakeReview{attachOK: true}
|
||||
h := handler.NewBackstageHandler(auth, brand, &fakeArticle{}, &fakeStreet{}, rev, nil)
|
||||
r := NewBackstage(BackstageOptions{Config: &config.Config{}, JWT: m, Backstage: h})
|
||||
return r, rev
|
||||
}
|
||||
|
||||
func reviewToken(t *testing.T, r *gin.Engine) *http.Cookie {
|
||||
t.Helper()
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/admin/login", strings.NewReader("account=admin&password=secret"))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
r.ServeHTTP(w, req)
|
||||
cookies := w.Result().Cookies()
|
||||
if len(cookies) == 0 {
|
||||
t.Fatal("登录未下发会话 cookie")
|
||||
}
|
||||
return cookies[0]
|
||||
}
|
||||
|
||||
// TestBackstageStreetAttachRedirect 批量并入应 302 跳回带 ?main= 的详情页,且参数透传到 service。
|
||||
func TestBackstageStreetAttachRedirect(t *testing.T) {
|
||||
r, rev := newReviewRouter(t)
|
||||
tok := reviewToken(t, r)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/admin/reviews/street/7/images/attach", strings.NewReader("main=1&img=2&img=3"))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.AddCookie(tok)
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusFound {
|
||||
t.Fatalf("并入应 302,实际 %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
if !strings.Contains(w.Header().Get("Location"), "/admin/reviews/street/7?main=1") {
|
||||
t.Fatalf("重定向应带 ?main=1,实际 %s", w.Header().Get("Location"))
|
||||
}
|
||||
if rev.last.op != "attach" || rev.last.mainID != 1 || len(rev.last.imgIDs) != 2 {
|
||||
t.Fatalf("参数透传错误: %+v", rev.last)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBackstageStreetAttachPrev 并入上一张应 302。
|
||||
func TestBackstageStreetAttachPrev(t *testing.T) {
|
||||
r, rev := newReviewRouter(t)
|
||||
tok := reviewToken(t, r)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/admin/reviews/street/7/images/2/attach-prev", nil)
|
||||
req.AddCookie(tok)
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusFound {
|
||||
t.Fatalf("并入上一张应 302,实际 %d", w.Code)
|
||||
}
|
||||
if rev.last.op != "attach-prev" || rev.last.imgID != 2 {
|
||||
t.Fatalf("参数透传错误: %+v", rev.last)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBackstageStreetDetach 拆出应 302 跳回「以该图为主图」的详情页。
|
||||
func TestBackstageStreetDetach(t *testing.T) {
|
||||
r, rev := newReviewRouter(t)
|
||||
tok := reviewToken(t, r)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/admin/reviews/street/7/images/2/detach", nil)
|
||||
req.AddCookie(tok)
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusFound {
|
||||
t.Fatalf("拆出应 302,实际 %d", w.Code)
|
||||
}
|
||||
if !strings.Contains(w.Header().Get("Location"), "/admin/reviews/street/7?main=2") {
|
||||
t.Fatalf("拆出后应以该图为主图跳转,实际 %s", w.Header().Get("Location"))
|
||||
}
|
||||
if rev.last.op != "detach" || rev.last.imgID != 2 {
|
||||
t.Fatalf("参数透传错误: %+v", rev.last)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBackstageStreetRenderGroups 详情页应按组折叠渲染(strip-group + 副图名)。
|
||||
func TestBackstageStreetRenderGroups(t *testing.T) {
|
||||
r, rev := newReviewRouter(t)
|
||||
tok := reviewToken(t, r)
|
||||
rev.view = &service.DraftDetailView{
|
||||
Kind: "street", ID: 7, Title: "T", Year: 2026, City: "Beijing", ImageCount: 3, Status: "pending",
|
||||
Images: []service.DraftImageRef{
|
||||
{ID: 1, Image: "main.jpg", Name: "main", IsDetail: 0},
|
||||
{ID: 2, Image: "det.jpg", Name: "detail-a", IsDetail: 1, ParentImageID: 1},
|
||||
{ID: 3, Image: "solo.jpg", Name: "solo", IsDetail: 0},
|
||||
},
|
||||
Groups: []service.DraftImageGroup{
|
||||
{Image: service.DraftImageRef{ID: 1, Image: "main.jpg", Name: "main"}, Details: []service.DraftImageRef{{ID: 2, Image: "det.jpg", Name: "detail-a"}}},
|
||||
{Image: service.DraftImageRef{ID: 3, Image: "solo.jpg", Name: "solo"}},
|
||||
},
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/admin/reviews/street/7?main=1", nil)
|
||||
req.AddCookie(tok)
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("详情应 200,实际 %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, "strip-group") {
|
||||
t.Fatalf("应按组折叠渲染 strip-group")
|
||||
}
|
||||
if !strings.Contains(body, "detail-a") {
|
||||
t.Fatalf("应渲染副图 detail-a")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user