feat: 分类系统 + 标签系统 — Phase 4 完成
分类:Model/Repo/Service/Controller + 管理后台树形页面 + 首页导航栏
标签:Model/Repo/Service/Controller + /tags/{slug} 落地页 + 写文章页输入
Post 加 CategoryID,Create/Update 支持分类和标签
SQL 迁移含 categories/tags/post_tags 表及预设未分类
This commit is contained in:
100
internal/controller/admin/category_controller.go
Normal file
100
internal/controller/admin/category_controller.go
Normal file
@ -0,0 +1,100 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"metazone.cc/mce/internal/common"
|
||||
"metazone.cc/mce/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CategoryController 分类管理控制器
|
||||
type CategoryController struct {
|
||||
svc categoryUseCase
|
||||
}
|
||||
|
||||
type categoryUseCase interface {
|
||||
Create(c *model.Category) error
|
||||
Update(c *model.Category) error
|
||||
Delete(id uint) error
|
||||
Tree() ([]model.Category, error)
|
||||
}
|
||||
|
||||
// NewCategoryController 构造函数
|
||||
func NewCategoryController(svc categoryUseCase) *CategoryController {
|
||||
return &CategoryController{svc: svc}
|
||||
}
|
||||
|
||||
// Page 分类管理页面
|
||||
func (cc *CategoryController) Page(c *gin.Context) {
|
||||
tree, _ := cc.svc.Tree()
|
||||
c.HTML(http.StatusOK, "admin/categories/index.html", common.BuildAdminPageData(c, gin.H{
|
||||
"Title": "分类管理",
|
||||
"ExtraCSS": "/admin/static/css/categories.css",
|
||||
"ExtraJS": "/admin/static/js/categories.js",
|
||||
"Tree": tree,
|
||||
}))
|
||||
}
|
||||
|
||||
// List 分类列表 API
|
||||
func (cc *CategoryController) List(c *gin.Context) {
|
||||
tree, err := cc.svc.Tree()
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusInternalServerError, "查询失败")
|
||||
return
|
||||
}
|
||||
if tree == nil {
|
||||
tree = []model.Category{}
|
||||
}
|
||||
common.Ok(c, tree)
|
||||
}
|
||||
|
||||
// Create 创建分类
|
||||
func (cc *CategoryController) Create(c *gin.Context) {
|
||||
var req model.Category
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, http.StatusBadRequest, "参数错误")
|
||||
return
|
||||
}
|
||||
if err := cc.svc.Create(&req); err != nil {
|
||||
common.Error(c, http.StatusInternalServerError, "创建失败")
|
||||
return
|
||||
}
|
||||
common.OkMessage(c, "分类已创建")
|
||||
}
|
||||
|
||||
// Update 更新分类
|
||||
func (cc *CategoryController) Update(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusBadRequest, "无效的 ID")
|
||||
return
|
||||
}
|
||||
var req model.Category
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.Error(c, http.StatusBadRequest, "参数错误")
|
||||
return
|
||||
}
|
||||
req.ID = uint(id)
|
||||
if err := cc.svc.Update(&req); err != nil {
|
||||
common.Error(c, http.StatusInternalServerError, "更新失败")
|
||||
return
|
||||
}
|
||||
common.OkMessage(c, "分类已更新")
|
||||
}
|
||||
|
||||
// Delete 删除分类
|
||||
func (cc *CategoryController) Delete(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusBadRequest, "无效的 ID")
|
||||
return
|
||||
}
|
||||
if err := cc.svc.Delete(uint(id)); err != nil {
|
||||
common.Error(c, http.StatusInternalServerError, "删除失败")
|
||||
return
|
||||
}
|
||||
common.OkMessage(c, "分类已删除")
|
||||
}
|
||||
@ -42,9 +42,9 @@ type postUseCase interface {
|
||||
|
||||
// studioUseCase StudioController 对 PostService 的最小依赖(ISP:9 个方法)
|
||||
type studioUseCase interface {
|
||||
Create(userID uint, title, body, visibility, postType, reprintSource, declaration string, reprintProhibited bool) (*model.Post, error)
|
||||
Create(userID uint, title, body, visibility, postType, reprintSource, declaration string, reprintProhibited bool, categoryID string) (*model.Post, error)
|
||||
GetByID(id uint) (*model.Post, error)
|
||||
Update(postID uint, title, body, visibility, postType, reprintSource, declaration string, reprintProhibited bool) error
|
||||
Update(postID uint, title, body, visibility, postType, reprintSource, declaration string, reprintProhibited bool, categoryID string) error
|
||||
Delete(postID uint) error
|
||||
SubmitForAudit(postID uint) error
|
||||
ListByUser(userID uint, status string, page, pageSize int) ([]model.Post, int64, error)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"metazone.cc/mce/internal/model"
|
||||
"metazone.cc/mce/internal/service"
|
||||
)
|
||||
|
||||
@ -9,11 +10,24 @@ type trendsUseCase interface {
|
||||
GetTrends(userID uint, since string) (*service.TrendResult, error)
|
||||
}
|
||||
|
||||
// tagParser ISP:标签解析
|
||||
type tagParser interface {
|
||||
ParseAndSave(tagStr string) ([]uint, error)
|
||||
SetPostTags(postID uint, tagStr string) error
|
||||
}
|
||||
|
||||
// StudioController 创作中心控制器(SSR 页面 + API)
|
||||
type StudioController struct {
|
||||
postService studioUseCase
|
||||
energySvc energyDeducter
|
||||
trendsSvc trendsUseCase
|
||||
tagSvc tagParser
|
||||
categorySvc categoryLoader
|
||||
}
|
||||
|
||||
// categoryLoader ISP:分类叶子节点查询
|
||||
type categoryLoader interface {
|
||||
GetLeaves() ([]model.Category, error)
|
||||
}
|
||||
|
||||
// NewStudioController 构造函数
|
||||
@ -32,3 +46,15 @@ func (ctrl *StudioController) WithTrendsService(svc trendsUseCase) *StudioContro
|
||||
ctrl.trendsSvc = svc
|
||||
return ctrl
|
||||
}
|
||||
|
||||
// WithTagService 链式注入标签服务
|
||||
func (ctrl *StudioController) WithTagService(svc tagParser) *StudioController {
|
||||
ctrl.tagSvc = svc
|
||||
return ctrl
|
||||
}
|
||||
|
||||
// WithCategoryService 链式注入分类服务
|
||||
func (ctrl *StudioController) WithCategoryService(svc categoryLoader) *StudioController {
|
||||
ctrl.categorySvc = svc
|
||||
return ctrl
|
||||
}
|
||||
|
||||
@ -30,12 +30,17 @@ func (ctrl *StudioController) Create(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
post, err := ctrl.postService.Create(uid, req.Title, req.Body, req.Visibility, req.PostType, req.ReprintSource, req.Declaration, req.ReprintProhibited)
|
||||
post, err := ctrl.postService.Create(uid, req.Title, req.Body, req.Visibility, req.PostType, req.ReprintSource, req.Declaration, req.ReprintProhibited, req.CategoryID)
|
||||
if err != nil {
|
||||
common.Error(c, http.StatusInternalServerError, "发布失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 关联标签
|
||||
if ctrl.tagSvc != nil && req.Tags != "" {
|
||||
_ = ctrl.tagSvc.SetPostTags(post.ID, req.Tags)
|
||||
}
|
||||
|
||||
common.OkWithMessage(c, post, "发布成功")
|
||||
}
|
||||
|
||||
@ -69,10 +74,15 @@ func (ctrl *StudioController) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := ctrl.postService.Update(uint(id), req.Title, req.Body, req.Visibility, req.PostType, req.ReprintSource, req.Declaration, req.ReprintProhibited); err != nil {
|
||||
if err := ctrl.postService.Update(uint(id), req.Title, req.Body, req.Visibility, req.PostType, req.ReprintSource, req.Declaration, req.ReprintProhibited, req.CategoryID); err != nil {
|
||||
common.Error(c, http.StatusInternalServerError, "编辑失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 更新标签
|
||||
if ctrl.tagSvc != nil {
|
||||
_ = ctrl.tagSvc.SetPostTags(uint(id), req.Tags)
|
||||
}
|
||||
|
||||
common.OkMessage(c, "编辑成功")
|
||||
}
|
||||
|
||||
@ -17,6 +17,16 @@ func (ctrl *StudioController) WritePage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 加载分类列表(叶子节点)
|
||||
var categories []interface{}
|
||||
if ctrl.categorySvc != nil {
|
||||
if leaves, err := ctrl.categorySvc.GetLeaves(); err == nil {
|
||||
for _, cat := range leaves {
|
||||
categories = append(categories, cat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 支持编辑模式:传入 post_id 参数
|
||||
postIDStr := c.Query("id")
|
||||
if postIDStr != "" {
|
||||
@ -32,10 +42,11 @@ func (ctrl *StudioController) WritePage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
c.HTML(http.StatusOK, "studio/write.html", common.BuildPageData(c, gin.H{
|
||||
"Title": "编辑文章 - 创作中心",
|
||||
"Post": post,
|
||||
"ActiveTab": "write",
|
||||
"ExtraCSS": "/static/css/studio.css",
|
||||
"Title": "编辑文章 - 创作中心",
|
||||
"Post": post,
|
||||
"ActiveTab": "write",
|
||||
"ExtraCSS": "/static/css/studio.css",
|
||||
"Categories": categories,
|
||||
}))
|
||||
return
|
||||
}
|
||||
@ -43,9 +54,10 @@ func (ctrl *StudioController) WritePage(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "studio/write.html", common.BuildPageData(c, gin.H{
|
||||
"Title": "写文章 - 创作中心",
|
||||
"ActiveTab": "write",
|
||||
"ExtraCSS": "/static/css/studio.css",
|
||||
"Title": "写文章 - 创作中心",
|
||||
"ActiveTab": "write",
|
||||
"ExtraCSS": "/static/css/studio.css",
|
||||
"Categories": categories,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
54
internal/controller/tag_controller.go
Normal file
54
internal/controller/tag_controller.go
Normal file
@ -0,0 +1,54 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"metazone.cc/mce/internal/common"
|
||||
"metazone.cc/mce/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// TagController 标签控制器(标签落地页)
|
||||
type TagController struct {
|
||||
svc tagUseCase
|
||||
}
|
||||
|
||||
type tagUseCase interface {
|
||||
GetPostsByTag(slug string, page, pageSize int) ([]model.Post, int64, *model.Tag, error)
|
||||
}
|
||||
|
||||
// NewTagController 构造函数
|
||||
func NewTagController(svc tagUseCase) *TagController {
|
||||
return &TagController{svc: svc}
|
||||
}
|
||||
|
||||
// TagPage 标签落地页
|
||||
func (tc *TagController) TagPage(c *gin.Context) {
|
||||
slug := c.Param("slug")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize := 20
|
||||
|
||||
posts, total, tag, err := tc.svc.GetPostsByTag(slug, page, pageSize)
|
||||
if err != nil {
|
||||
c.HTML(http.StatusNotFound, "posts/404.html", common.BuildPageData(c, gin.H{
|
||||
"Title": "未找到",
|
||||
"ErrorMsg": "该标签不存在",
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
totalPages := common.PageCount(total, pageSize)
|
||||
c.HTML(http.StatusOK, "tags/detail.html", common.BuildPageData(c, gin.H{
|
||||
"Title": "标签:" + tag.Name,
|
||||
"ExtraCSS": "/static/css/posts.css",
|
||||
"Posts": posts,
|
||||
"Total": total,
|
||||
"Page": page,
|
||||
"TotalPages": totalPages,
|
||||
"PrevPage": max(1, page-1),
|
||||
"NextPage": min(totalPages, page+1),
|
||||
"Tag": tag,
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user