This commit is contained in:
toom1996
2026-09-28 10:52:50 +08:00
parent aaeab41d67
commit d15d2a4701
42 changed files with 19884 additions and 337 deletions

View File

@ -89,11 +89,10 @@ func NewBackstage(opt BackstageOptions) *gin.Engine {
admin.POST("/brands", opt.Backstage.BrandCreate)
admin.GET("/brands/:id", opt.Backstage.BrandEdit)
admin.POST("/brands/:id", opt.Backstage.BrandUpdate)
// 文章管理:走秀 + 街拍混合列表,替代原「审核页 + 分类列表」,是编辑/通过/驳回/下架的统一入口。
// 文章管理:走秀 + 街拍混合列表,替代原「审核页 + 分类列表」,是编辑/通过/删除(下架)的统一入口。
admin.GET("/articles", opt.Backstage.Articles)
admin.POST("/articles/batch", opt.Backstage.ArticleBatch)
admin.POST("/articles/:kind/:id/approve", opt.Backstage.ArticleApprove)
admin.POST("/articles/:kind/:id/reject", opt.Backstage.ArticleReject)
// 街拍主副图分组(零 JS:同步表单 POST,成功 302 回编辑页;AJAX 版见路由上方)。
admin.POST("/articles/:kind/:id/images/attach", opt.Backstage.ArticleAttachImages)
admin.POST("/articles/:kind/:id/images/:img/detach", opt.Backstage.ArticleDetachImage)

View File

@ -147,6 +147,9 @@ func (f *fakeArticle) SetDeleted(ctx context.Context, id uint32, deleted uint8)
func (f *fakeArticle) PurgeGallery(ctx context.Context, id uint32) error {
return nil
}
func (f *fakeArticle) GetRunwayDupSources(ctx context.Context, imageIDs []uint32) (map[uint32]dto.DupSource, error) {
return map[uint32]dto.DupSource{}, nil
}
type fakeStreet struct{}
@ -204,6 +207,9 @@ func (f *fakeStreet) SetDeleted(ctx context.Context, id uint32, deleted uint8) e
func (f *fakeStreet) PurgeGallery(ctx context.Context, id uint32) error {
return nil
}
func (f *fakeStreet) GetStreetSnapDupSources(ctx context.Context, imageIDs []uint32) (map[uint32]dto.DupSource, error) {
return map[uint32]dto.DupSource{}, nil
}
// fakeAdminRepo 实现文章管理仓储接口;返回固定的混合列表示例,供路由/模板渲染测试。
type fakeAdminRepo struct{}
@ -254,6 +260,9 @@ func (f *fakeAdminRepo) CountMixed(_ context.Context, status, kind string) (int6
}
return 0, nil
}
func (f *fakeAdminRepo) DupRateByRecords(_ context.Context, kind string, ids []uint32) (map[uint32]repository.DupStat, error) {
return map[uint32]repository.DupStat{}, nil
}
// ── 冒烟测试:验证路由 / 鉴权 / 模板渲染(无需真实数据库)─────────────────
@ -393,7 +402,6 @@ type fakeReview struct {
service.ReviewService
attachOK bool
approveErr error
rejectErr error
last struct {
kind string
recordID uint32
@ -407,7 +415,6 @@ type fakeReview struct {
Action string
Kind string
ID uint32
Reason string
}
view *service.RecordDetailView
}
@ -456,8 +463,7 @@ func (f *fakeReview) Approve(_ context.Context, kind string, id uint32, reviewer
Action string
Kind string
ID uint32
Reason string
}{"approve", kind, id, ""})
}{"approve", kind, id})
// last 供「表单式通过」用例断言(批量接口用 calls)。
f.last.kind, f.last.recordID, f.last.op = kind, id, "approve"
if f.approveErr != nil {
@ -466,16 +472,6 @@ func (f *fakeReview) Approve(_ context.Context, kind string, id uint32, reviewer
return id, nil
}
func (f *fakeReview) Reject(_ context.Context, kind string, id uint32, reviewer, reason string) error {
f.calls = append(f.calls, struct {
Action string
Kind string
ID uint32
Reason string
}{"reject", kind, id, reason})
return f.rejectErr
}
func newReviewRouter(t *testing.T) (*gin.Engine, *fakeReview) {
t.Helper()
m := jwt.NewManager("test-secret", 168)
@ -697,26 +693,6 @@ func TestBackstageReviewBatchApprove(t *testing.T) {
}
}
// TestBackstageReviewBatchRejectNeedsReason 驳回必须带理由(留痕要求),否则 400 且不调用 service。
func TestBackstageReviewBatchRejectNeedsReason(t *testing.T) {
r, rev := newReviewRouter(t)
tok := reviewToken(t, r)
body := `{"action":"reject","items":[{"kind":"street","id":11}]}`
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/admin/api/articles/batch", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.AddCookie(tok)
r.ServeHTTP(w, req)
if w.Code != 400 {
t.Fatalf("缺理由应 400: %d %s", w.Code, w.Body.String())
}
if len(rev.calls) != 0 {
t.Fatalf("缺理由不应调用 service: %+v", rev.calls)
}
}
// TestBackstageReviewBatchPartialFailure 未注册类型只计失败,其余照常处理(不整体回滚)。
func TestBackstageReviewBatchPartialFailure(t *testing.T) {
r, rev := newReviewRouter(t)