refactor(publish): 审核侧切到正式表,通过即改状态不再晋升

This commit is contained in:
toom1996
2026-09-23 11:09:12 +08:00
parent e0ce81318d
commit 76626b8372
19 changed files with 423 additions and 775 deletions

View File

@ -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)

View File

@ -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 后台的样式与脚本由二进制内嵌提供:零外部依赖、零构建产物。