refactor(publish): 审核侧切到正式表,通过即改状态不再晋升
This commit is contained in:
@ -80,8 +80,8 @@ func NewBackstage(opt BackstageOptions) *gin.Engine {
|
||||
// 审核页的 AJAX 接口(返回 JSON,不整页跳转):批量通过/驳回、主副图合并与拆出。
|
||||
// 与下面的表单式路由并存 —— 表单路径保持「无 JS 也能用」,AJAX 只是更快的那条路。
|
||||
admin.POST("/api/reviews/batch", opt.Backstage.ReviewBatch)
|
||||
admin.POST("/api/reviews/:kind/:id/images/attach", opt.Backstage.ReviewAttachImages)
|
||||
admin.POST("/api/reviews/:kind/:id/images/:img/detach", opt.Backstage.ReviewDetachImage)
|
||||
admin.POST("/api/reviews/:kind/:id/images/attach", opt.Backstage.ReviewAttachImagesAPI)
|
||||
admin.POST("/api/reviews/:kind/:id/images/:img/detach", opt.Backstage.ReviewDetachImageAPI)
|
||||
|
||||
admin.GET("/", opt.Backstage.Dashboard)
|
||||
admin.GET("/brands", opt.Backstage.Brands)
|
||||
@ -103,17 +103,17 @@ func NewBackstage(opt BackstageOptions) *gin.Engine {
|
||||
admin.POST("/street-snaps/:id/images/:img/delete", opt.Backstage.StreetSnapImageDelete)
|
||||
admin.POST("/street-snaps/:id/cover", opt.Backstage.StreetSnapSetCover)
|
||||
admin.POST("/street-snaps/:id/toggle", opt.Backstage.StreetSnapToggleDeleted)
|
||||
// 审核草稿:爬虫入库待人工审核 → 通过晋升正式表 / 驳回。
|
||||
// 审核记录:爬虫入库待人工审核 → 通过置为已发布 / 驳回。
|
||||
// 路由带 :kind 段(runway / street / 缺省=全部),详情/通过/驳回均按 kind 分流。
|
||||
admin.GET("/reviews", opt.Backstage.ReviewList)
|
||||
admin.GET("/reviews/:kind", opt.Backstage.ReviewList)
|
||||
admin.GET("/reviews/:kind/:id", opt.Backstage.ReviewDetail)
|
||||
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)
|
||||
admin.POST("/reviews/:kind/:id/images/:img/delete", opt.Backstage.ReviewImageDelete)
|
||||
// 街拍主副图分组操作(零 JS:同步表单 POST,成功 302 回详情页)。
|
||||
admin.POST("/reviews/:kind/:id/images/attach", opt.Backstage.ReviewAttachDraftImages)
|
||||
admin.POST("/reviews/:kind/:id/images/:img/detach", opt.Backstage.ReviewDetachDraftImage)
|
||||
admin.POST("/reviews/:kind/:id/images/attach", opt.Backstage.ReviewAttachImages)
|
||||
admin.POST("/reviews/:kind/:id/images/:img/detach", opt.Backstage.ReviewDetachImage)
|
||||
// 爬虫入库任务监控:列出处理进度 + 失败重试
|
||||
admin.GET("/ingest-jobs", opt.Backstage.IngestJobs)
|
||||
admin.POST("/ingest-jobs/:id/retry", opt.Backstage.IngestJobRetry)
|
||||
|
||||
@ -333,12 +333,12 @@ type fakeReview struct {
|
||||
approveErr error
|
||||
rejectErr error
|
||||
last struct {
|
||||
kind string
|
||||
draftID uint32
|
||||
mainID uint32
|
||||
imgIDs []uint32
|
||||
imgID uint32
|
||||
op string
|
||||
kind string
|
||||
recordID uint32
|
||||
mainID uint32
|
||||
imgIDs []uint32
|
||||
imgID uint32
|
||||
op string
|
||||
}
|
||||
// calls 记录批量审核的逐条调用,供断言。
|
||||
calls []struct {
|
||||
@ -347,27 +347,32 @@ type fakeReview struct {
|
||||
ID uint32
|
||||
Reason string
|
||||
}
|
||||
view *service.DraftDetailView
|
||||
view *service.RecordDetailView
|
||||
}
|
||||
|
||||
var _ service.ReviewService = (*fakeReview)(nil)
|
||||
|
||||
func (f *fakeReview) DraftDetail(_ context.Context, _ string, _ uint32) (*service.DraftDetailView, error) {
|
||||
func (f *fakeReview) RecordDetail(_ context.Context, _ string, _ uint32) (*service.RecordDetailView, 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
|
||||
return &service.RecordDetailView{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"
|
||||
// UpdateRecord 表单式通过路径会先应用字段微调;这里只做无操作桩(真正的白名单在仓储层)。
|
||||
func (f *fakeReview) UpdateRecord(_ context.Context, _ string, _ uint32, _ map[string]any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeReview) AttachImages(_ context.Context, kind string, recordID, mainID uint32, imgIDs []uint32) error {
|
||||
f.last.kind, f.last.recordID, f.last.mainID, f.last.imgIDs, f.last.op = kind, recordID, mainID, imgIDs, "attach"
|
||||
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"
|
||||
func (f *fakeReview) DetachImage(_ context.Context, kind string, recordID, imgID uint32) error {
|
||||
f.last.kind, f.last.recordID, f.last.imgID, f.last.op = kind, recordID, imgID, "detach"
|
||||
if !f.attachOK {
|
||||
return errors.New("主图无效")
|
||||
}
|
||||
@ -391,6 +396,8 @@ func (f *fakeReview) Approve(_ context.Context, kind string, id uint32, reviewer
|
||||
ID uint32
|
||||
Reason string
|
||||
}{"approve", kind, id, ""})
|
||||
// last 供「表单式通过」用例断言(批量接口用 calls)。
|
||||
f.last.kind, f.last.recordID, f.last.op = kind, id, "approve"
|
||||
if f.approveErr != nil {
|
||||
return 0, f.approveErr
|
||||
}
|
||||
@ -494,16 +501,16 @@ func TestBackstageStreetDetach(t *testing.T) {
|
||||
func TestBackstageStreetRenderGroups(t *testing.T) {
|
||||
r, rev := newReviewRouter(t)
|
||||
tok := reviewToken(t, r)
|
||||
rev.view = &service.DraftDetailView{
|
||||
rev.view = &service.RecordDetailView{
|
||||
Kind: "street", ID: 7, Title: "T", Year: 2026, City: "Beijing", ImageCount: 3, Status: "pending",
|
||||
Images: []service.DraftImageRef{
|
||||
Images: []service.RecordImageRef{
|
||||
{ID: 1, Image: "main.jpg", Name: "main", IsDetail: 0, DetailCount: 1},
|
||||
{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"}},
|
||||
Groups: []service.ImageGroup{
|
||||
{Image: service.RecordImageRef{ID: 1, Image: "main.jpg", Name: "main"}, Details: []service.RecordImageRef{{ID: 2, Image: "det.jpg", Name: "detail-a"}}},
|
||||
{Image: service.RecordImageRef{ID: 3, Image: "solo.jpg", Name: "solo"}},
|
||||
},
|
||||
}
|
||||
|
||||
@ -538,6 +545,26 @@ func TestBackstageStreetRenderGroups(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestBackstageApprovePublishes 通过 = 只改状态,不再晋升重建。
|
||||
// 断言表单式通过路径确实走到了 service.Approve(而不是别的方法)。
|
||||
func TestBackstageApprovePublishes(t *testing.T) {
|
||||
r, rev := newReviewRouter(t)
|
||||
tok := reviewToken(t, r)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/admin/reviews/street/7/approve", nil)
|
||||
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 rev.last.op != "approve" {
|
||||
t.Fatalf("应调用 Approve,实际 op=%s", rev.last.op)
|
||||
}
|
||||
}
|
||||
|
||||
// ── 新增:静态资源、导航统计 & 批量审核 AJAX 接口 ───────────────────────────
|
||||
|
||||
// TestBackstageAssets 后台的样式与脚本由二进制内嵌提供:零外部依赖、零构建产物。
|
||||
|
||||
Reference in New Issue
Block a user