first commit
This commit is contained in:
425
app/blog/controllers/blog.go
Normal file
425
app/blog/controllers/blog.go
Normal file
@@ -0,0 +1,425 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
blogModels "goaresv3/app/blog/models"
|
||||
"goaresv3/config"
|
||||
)
|
||||
|
||||
type UpsertCategoryRequest struct {
|
||||
Title string `json:"title" binding:"required,max=254"`
|
||||
Slug string `json:"slug" binding:"required,max=254"`
|
||||
Description string `json:"description"`
|
||||
ParentID *uint `json:"parent_id"`
|
||||
}
|
||||
|
||||
type UpsertTagRequest struct {
|
||||
Name string `json:"name" binding:"required,max=254"`
|
||||
}
|
||||
|
||||
type UpsertPostRequest struct {
|
||||
Title string `json:"title" binding:"required,max=254"`
|
||||
Images string `json:"images" binding:"required"`
|
||||
ImagesMid string `json:"images_mid" binding:"required"`
|
||||
ImagesMin string `json:"images_min" binding:"required"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
Quality int `json:"quality"`
|
||||
Format string `json:"format" binding:"omitempty,max=10"`
|
||||
Content string `json:"content"`
|
||||
Slug string `json:"slug" binding:"required,max=254"`
|
||||
CategoryIDs []uint `json:"category_ids"`
|
||||
TagIDs []uint `json:"tag_ids"`
|
||||
}
|
||||
|
||||
func parseBlogID(c *gin.Context) (uint, bool) {
|
||||
id, err := strconv.ParseUint(strings.TrimSpace(c.Param("id")), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||
return 0, false
|
||||
}
|
||||
return uint(id), true
|
||||
}
|
||||
|
||||
// ListCategories godoc
|
||||
// @Summary List blog categories
|
||||
// @Tags Blog
|
||||
// @Produce json
|
||||
// @Success 200 {array} map[string]interface{}
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /api/v1/blog/categories [get]
|
||||
func ListCategories(c *gin.Context) {
|
||||
var items []blogModels.Category
|
||||
if err := config.DB.Preload("Children").Order("id DESC").Find(&items).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch categories"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// CreateCategory godoc
|
||||
// @Summary Create blog category
|
||||
// @Tags Blog
|
||||
// @Security BearerAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body UpsertCategoryRequest true "category payload"
|
||||
// @Success 201 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 409 {object} map[string]string
|
||||
// @Router /api/v1/blog/categories [post]
|
||||
func CreateCategory(c *gin.Context) {
|
||||
var req UpsertCategoryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
item := blogModels.Category{
|
||||
Title: req.Title,
|
||||
Slug: req.Slug,
|
||||
Description: req.Description,
|
||||
ParentID: req.ParentID,
|
||||
}
|
||||
if err := config.DB.Create(&item).Error; err != nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "failed to create category"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, item)
|
||||
}
|
||||
|
||||
// UpdateCategory godoc
|
||||
// @Summary Update blog category
|
||||
// @Tags Blog
|
||||
// @Security BearerAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "category id"
|
||||
// @Param request body UpsertCategoryRequest true "category payload"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Failure 409 {object} map[string]string
|
||||
// @Router /api/v1/blog/categories/{id} [put]
|
||||
func UpdateCategory(c *gin.Context) {
|
||||
id, ok := parseBlogID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req UpsertCategoryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var item blogModels.Category
|
||||
if err := config.DB.First(&item, id).Error; errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "category not found"})
|
||||
return
|
||||
}
|
||||
if err := config.DB.Model(&item).Updates(map[string]any{
|
||||
"title": req.Title,
|
||||
"slug": req.Slug,
|
||||
"description": req.Description,
|
||||
"parent_id": req.ParentID,
|
||||
}).Error; err != nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "failed to update category"})
|
||||
return
|
||||
}
|
||||
_ = config.DB.First(&item, id).Error
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// DeleteCategory godoc
|
||||
// @Summary Delete blog category
|
||||
// @Tags Blog
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Param id path int true "category id"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /api/v1/blog/categories/{id} [delete]
|
||||
func DeleteCategory(c *gin.Context) {
|
||||
id, ok := parseBlogID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
res := config.DB.Delete(&blogModels.Category{}, id)
|
||||
if res.RowsAffected == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "category not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "category deleted"})
|
||||
}
|
||||
|
||||
// ListTags godoc
|
||||
// @Summary List blog tags
|
||||
// @Tags Blog
|
||||
// @Produce json
|
||||
// @Success 200 {array} map[string]interface{}
|
||||
// @Router /api/v1/blog/tags [get]
|
||||
func ListTags(c *gin.Context) {
|
||||
var items []blogModels.Tag
|
||||
if err := config.DB.Order("id DESC").Find(&items).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch tags"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// CreateTag godoc
|
||||
// @Summary Create blog tag
|
||||
// @Tags Blog
|
||||
// @Security BearerAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body UpsertTagRequest true "tag payload"
|
||||
// @Success 201 {object} map[string]interface{}
|
||||
// @Router /api/v1/blog/tags [post]
|
||||
func CreateTag(c *gin.Context) {
|
||||
var req UpsertTagRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
item := blogModels.Tag{Name: req.Name}
|
||||
if err := config.DB.Create(&item).Error; err != nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "failed to create tag"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, item)
|
||||
}
|
||||
|
||||
// UpdateTag godoc
|
||||
// @Summary Update blog tag
|
||||
// @Tags Blog
|
||||
// @Security BearerAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "tag id"
|
||||
// @Param request body UpsertTagRequest true "tag payload"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/v1/blog/tags/{id} [put]
|
||||
func UpdateTag(c *gin.Context) {
|
||||
id, ok := parseBlogID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req UpsertTagRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var item blogModels.Tag
|
||||
if err := config.DB.First(&item, id).Error; errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "tag not found"})
|
||||
return
|
||||
}
|
||||
if err := config.DB.Model(&item).Update("name", req.Name).Error; err != nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "failed to update tag"})
|
||||
return
|
||||
}
|
||||
_ = config.DB.First(&item, id).Error
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// DeleteTag godoc
|
||||
// @Summary Delete blog tag
|
||||
// @Tags Blog
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Param id path int true "tag id"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Router /api/v1/blog/tags/{id} [delete]
|
||||
func DeleteTag(c *gin.Context) {
|
||||
id, ok := parseBlogID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
res := config.DB.Delete(&blogModels.Tag{}, id)
|
||||
if res.RowsAffected == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "tag not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "tag deleted"})
|
||||
}
|
||||
|
||||
// ListPosts godoc
|
||||
// @Summary List blog posts
|
||||
// @Tags Blog
|
||||
// @Produce json
|
||||
// @Success 200 {array} map[string]interface{}
|
||||
// @Router /api/v1/blog/posts [get]
|
||||
func ListPosts(c *gin.Context) {
|
||||
var items []blogModels.Post
|
||||
if err := config.DB.Preload("Categories").Preload("Tags").Order("id DESC").Find(&items).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch posts"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
// GetPost godoc
|
||||
// @Summary Get blog post
|
||||
// @Tags Blog
|
||||
// @Produce json
|
||||
// @Param id path int true "post id"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /api/v1/blog/posts/{id} [get]
|
||||
func GetPost(c *gin.Context) {
|
||||
id, ok := parseBlogID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var item blogModels.Post
|
||||
if err := config.DB.Preload("Categories").Preload("Tags").First(&item, id).Error; errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "post not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// CreatePost godoc
|
||||
// @Summary Create blog post
|
||||
// @Tags Blog
|
||||
// @Security BearerAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body UpsertPostRequest true "post payload"
|
||||
// @Success 201 {object} map[string]interface{}
|
||||
// @Router /api/v1/blog/posts [post]
|
||||
func CreatePost(c *gin.Context) {
|
||||
var req UpsertPostRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
item := blogModels.Post{
|
||||
Title: req.Title,
|
||||
Images: req.Images,
|
||||
ImagesMid: req.ImagesMid,
|
||||
ImagesMin: req.ImagesMin,
|
||||
Width: req.Width,
|
||||
Height: req.Height,
|
||||
Quality: req.Quality,
|
||||
Format: req.Format,
|
||||
Content: req.Content,
|
||||
Slug: req.Slug,
|
||||
}
|
||||
if err := config.DB.Create(&item).Error; err != nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "failed to create post"})
|
||||
return
|
||||
}
|
||||
if err := assignPostRelations(item.ID, req.CategoryIDs, req.TagIDs); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to assign relations"})
|
||||
return
|
||||
}
|
||||
_ = config.DB.Preload("Categories").Preload("Tags").First(&item, item.ID).Error
|
||||
c.JSON(http.StatusCreated, item)
|
||||
}
|
||||
|
||||
// UpdatePost godoc
|
||||
// @Summary Update blog post
|
||||
// @Tags Blog
|
||||
// @Security BearerAuth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "post id"
|
||||
// @Param request body UpsertPostRequest true "post payload"
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Router /api/v1/blog/posts/{id} [put]
|
||||
func UpdatePost(c *gin.Context) {
|
||||
id, ok := parseBlogID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req UpsertPostRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var item blogModels.Post
|
||||
if err := config.DB.First(&item, id).Error; errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "post not found"})
|
||||
return
|
||||
}
|
||||
if err := config.DB.Model(&item).Updates(map[string]any{
|
||||
"title": req.Title,
|
||||
"images": req.Images,
|
||||
"images_mid": req.ImagesMid,
|
||||
"images_min": req.ImagesMin,
|
||||
"width": req.Width,
|
||||
"height": req.Height,
|
||||
"quality": req.Quality,
|
||||
"format": req.Format,
|
||||
"content": req.Content,
|
||||
"slug": req.Slug,
|
||||
}).Error; err != nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "failed to update post"})
|
||||
return
|
||||
}
|
||||
if err := assignPostRelations(item.ID, req.CategoryIDs, req.TagIDs); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to assign relations"})
|
||||
return
|
||||
}
|
||||
_ = config.DB.Preload("Categories").Preload("Tags").First(&item, item.ID).Error
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// DeletePost godoc
|
||||
// @Summary Delete blog post
|
||||
// @Tags Blog
|
||||
// @Security BearerAuth
|
||||
// @Produce json
|
||||
// @Param id path int true "post id"
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Router /api/v1/blog/posts/{id} [delete]
|
||||
func DeletePost(c *gin.Context) {
|
||||
id, ok := parseBlogID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
res := config.DB.Delete(&blogModels.Post{}, id)
|
||||
if res.RowsAffected == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "post not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "post deleted"})
|
||||
}
|
||||
|
||||
func assignPostRelations(postID uint, categoryIDs, tagIDs []uint) error {
|
||||
var p blogModels.Post
|
||||
if err := config.DB.First(&p, postID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if categoryIDs != nil {
|
||||
var categories []blogModels.Category
|
||||
if len(categoryIDs) > 0 {
|
||||
if err := config.DB.Where("id IN ?", categoryIDs).Find(&categories).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := config.DB.Model(&p).Association("Categories").Replace(categories); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if tagIDs != nil {
|
||||
var tags []blogModels.Tag
|
||||
if len(tagIDs) > 0 {
|
||||
if err := config.DB.Where("id IN ?", tagIDs).Find(&tags).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := config.DB.Model(&p).Association("Tags").Replace(tags); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
53
app/blog/models/blog.go
Normal file
53
app/blog/models/blog.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Minimal, temiz GORM modelleri
|
||||
|
||||
type Category struct {
|
||||
gorm.Model
|
||||
Title string `gorm:"type:varchar(254);not null" json:"title"`
|
||||
Slug string `gorm:"type:varchar(254);not null;uniqueIndex" json:"slug"`
|
||||
Description string `json:"description,omitempty"`
|
||||
ParentID *uint `json:"parent_id,omitempty"`
|
||||
Parent *Category `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;foreignKey:ParentID" json:"parent,omitempty"`
|
||||
Children []Category `gorm:"foreignKey:ParentID" json:"children,omitempty"`
|
||||
Posts []Post `gorm:"many2many:post_categories;" json:"posts,omitempty"`
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
gorm.Model
|
||||
Name string `gorm:"type:varchar(254);not null" json:"name"`
|
||||
Posts []Post `gorm:"many2many:post_tags;" json:"posts,omitempty"`
|
||||
}
|
||||
|
||||
type Post struct {
|
||||
gorm.Model
|
||||
Title string `gorm:"type:varchar(254);not null" json:"title" form:"title"`
|
||||
Images string `gorm:"type:text;not null" json:"images" form:"images"`
|
||||
ImagesMid string `gorm:"type:text;not null" json:"images_mid" form:"images_mid"`
|
||||
ImagesMin string `gorm:"type:text;not null" json:"images_min" form:"images_min"`
|
||||
Width int `gorm:"default:0" json:"width" form:"width"`
|
||||
Height int `gorm:"default:0" json:"height" form:"height"`
|
||||
Quality int `gorm:"default:0" json:"quality" form:"quality"`
|
||||
Format string `gorm:"type:varchar(10)" json:"format" form:"format" default:"avif"`
|
||||
Content string `gorm:"type:text" json:"content,omitempty" form:"content"`
|
||||
Slug string `gorm:"type:varchar(254);not null;uniqueIndex" json:"slug" form:"slug"`
|
||||
Categories []Category `gorm:"many2many:post_categories;" json:"categories,omitempty" form:"categories"`
|
||||
Tags []Tag `gorm:"many2many:post_tags;" json:"tags,omitempty" form:"tags"`
|
||||
}
|
||||
|
||||
type CategoryView struct {
|
||||
gorm.Model
|
||||
CategoryID uint `json:"category_id"`
|
||||
IPAddress string `gorm:"type:varchar(45)" json:"ip_address,omitempty"`
|
||||
}
|
||||
|
||||
type Comment struct {
|
||||
gorm.Model
|
||||
UserID uint `json:"user_id"`
|
||||
PostID uint `json:"post_id"`
|
||||
Body string `gorm:"type:text" json:"body,omitempty"`
|
||||
}
|
||||
Reference in New Issue
Block a user