603 lines
19 KiB
Go
603 lines
19 KiB
Go
package v1
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
"github.com/gochat/gochat/internal/service"
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
"github.com/gochat/gochat/pkg/pagination"
|
|
"github.com/gochat/gochat/pkg/response"
|
|
)
|
|
|
|
// ArticleHandler handles Article CRUD and Knowledge Base endpoints.
|
|
type ArticleHandler struct {
|
|
svc *service.ArticleService
|
|
portalSvc *service.PortalService
|
|
}
|
|
|
|
// NewArticleHandler creates a new ArticleHandler.
|
|
func NewArticleHandler(svc *service.ArticleService, portalSvc ...*service.PortalService) *ArticleHandler {
|
|
h := &ArticleHandler{svc: svc}
|
|
if len(portalSvc) > 0 {
|
|
h.portalSvc = portalSvc[0]
|
|
}
|
|
return h
|
|
}
|
|
|
|
// Create creates a new article.
|
|
// POST /portals/:portal_id/articles
|
|
func (h *ArticleHandler) Create(c *gin.Context) {
|
|
accountID := getAccountID(c)
|
|
if accountID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
portal, ok := h.resolvePortal(c, accountID)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
var req service.CreateArticleRequest
|
|
if err := bindChatwootPayload(c, "article", &req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
article, err := h.svc.CreateWithAccount(c.Request.Context(), accountID, portal.ID, currentUserID(c), &req)
|
|
if err != nil {
|
|
applogger.L().Errorf("Create article: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to create article")
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"payload": articlePayload(article)})
|
|
}
|
|
|
|
// Get retrieves an article by ID and increments its view count.
|
|
// GET /portals/:portal_id/articles/:id
|
|
func (h *ArticleHandler) Get(c *gin.Context) {
|
|
portal, ok := h.resolvePortalForRequest(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
id, err := articleIDParam(c)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid article id")
|
|
return
|
|
}
|
|
|
|
article, err := h.svc.GetByPortalAndID(c.Request.Context(), portal.ID, id)
|
|
if err != nil {
|
|
applogger.L().Errorf("Get article: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "article not found")
|
|
return
|
|
}
|
|
|
|
// Increment views on each read (non-blocking; log error only)
|
|
if err := h.svc.IncrementViews(c.Request.Context(), id); err != nil {
|
|
applogger.L().Warnf("IncrementViews article %d: %v", id, err)
|
|
} else {
|
|
article.Views++
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"payload": articlePayload(article)})
|
|
}
|
|
|
|
// Edit retrieves an article in edit context (full content without incrementing views).
|
|
// GET /portals/:portal_id/articles/:id/edit
|
|
func (h *ArticleHandler) Edit(c *gin.Context) {
|
|
portal, ok := h.resolvePortalForRequest(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
id, err := articleIDParam(c)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid article id")
|
|
return
|
|
}
|
|
|
|
article, err := h.svc.GetByPortalAndID(c.Request.Context(), portal.ID, id)
|
|
if err != nil {
|
|
applogger.L().Errorf("Edit article: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "article not found")
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"payload": articlePayload(article)})
|
|
}
|
|
|
|
// Update modifies an existing article.
|
|
// PUT /portals/:portal_id/articles/:id
|
|
func (h *ArticleHandler) Update(c *gin.Context) {
|
|
portal, ok := h.resolvePortalForRequest(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
id, err := articleIDParam(c)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid article id")
|
|
return
|
|
}
|
|
|
|
var req service.UpdateArticleRequest
|
|
if err := bindChatwootPayload(c, "article", &req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
article, err := h.svc.UpdateScoped(c.Request.Context(), portal.ID, id, &req)
|
|
if err != nil {
|
|
applogger.L().Errorf("Update article: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to update article")
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"payload": articlePayload(article)})
|
|
}
|
|
|
|
// Delete removes an article.
|
|
// DELETE /portals/:portal_id/articles/:id
|
|
func (h *ArticleHandler) Delete(c *gin.Context) {
|
|
portal, ok := h.resolvePortalForRequest(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
id, err := articleIDParam(c)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid article id")
|
|
return
|
|
}
|
|
|
|
if err := h.svc.DeleteScoped(c.Request.Context(), portal.ID, id); err != nil {
|
|
applogger.L().Errorf("Delete article: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, "failed to delete article")
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusOK)
|
|
}
|
|
|
|
// List returns all articles for a portal (paginated).
|
|
// GET /portals/:portal_id/articles
|
|
func (h *ArticleHandler) List(c *gin.Context) {
|
|
accountID := getAccountID(c)
|
|
if accountID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
portal, ok := h.resolvePortal(c, accountID)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
p := pagination.Parse(c)
|
|
params, ok := h.articleSearchParams(c, portal.ID, p.Offset, p.PerPage)
|
|
if !ok {
|
|
return
|
|
}
|
|
articles, _, svcErr := h.svc.Search(c.Request.Context(), params)
|
|
if svcErr != nil {
|
|
applogger.L().Errorf("List articles: %v", svcErr)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to list articles")
|
|
return
|
|
}
|
|
meta, svcErr := h.svc.ListMeta(c.Request.Context(), params, currentUserID(c))
|
|
if svcErr != nil {
|
|
applogger.L().Errorf("Article list meta: %v", svcErr)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to list articles")
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"payload": articlePayloads(articles), "meta": articleListMetaPayload(meta, p.Page)})
|
|
}
|
|
|
|
// ListByCategory returns articles for a specific category (paginated).
|
|
// GET /portals/:portal_id/categories/:category_id/articles
|
|
func (h *ArticleHandler) ListByCategory(c *gin.Context) {
|
|
categoryID, err := strconv.ParseUint(c.Param("category_id"), 10, 64)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid category_id")
|
|
return
|
|
}
|
|
|
|
p := pagination.Parse(c)
|
|
|
|
articles, count, svcErr := h.svc.ListByCategoryID(c.Request.Context(), uint(categoryID), p.Page, p.PerPage)
|
|
if svcErr != nil {
|
|
applogger.L().Errorf("List articles by category: %v", svcErr)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to list articles")
|
|
return
|
|
}
|
|
|
|
response.OKWithMeta(c, articles, p.Page, p.PerPage, count)
|
|
}
|
|
|
|
// Search returns filtered articles based on query parameters.
|
|
// GET /portals/:portal_id/articles/search
|
|
func (h *ArticleHandler) Search(c *gin.Context) {
|
|
accountID := getAccountID(c)
|
|
if accountID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
portal, ok := h.resolvePortal(c, accountID)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
p := pagination.Parse(c)
|
|
params, ok := h.articleSearchParams(c, portal.ID, p.Offset, p.PerPage)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
articles, count, svcErr := h.svc.Search(c.Request.Context(), params)
|
|
if svcErr != nil {
|
|
applogger.L().Errorf("Search articles: %v", svcErr)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to search articles")
|
|
return
|
|
}
|
|
|
|
meta, svcErr := h.svc.ListMeta(c.Request.Context(), params, currentUserID(c))
|
|
if svcErr != nil {
|
|
applogger.L().Errorf("Article search meta: %v", svcErr)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to search articles")
|
|
return
|
|
}
|
|
meta.ArticlesCount = count
|
|
c.JSON(http.StatusOK, gin.H{"payload": articlePayloads(articles), "meta": articleListMetaPayload(meta, p.Page)})
|
|
}
|
|
|
|
// StatusCounts returns article counts grouped by status.
|
|
// GET /portals/:portal_id/articles/status_counts
|
|
func (h *ArticleHandler) StatusCounts(c *gin.Context) {
|
|
portalID, err := strconv.ParseUint(c.Param("portal_id"), 10, 64)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid portal_id")
|
|
return
|
|
}
|
|
|
|
counts, svcErr := h.svc.StatusCounts(c.Request.Context(), uint(portalID))
|
|
if svcErr != nil {
|
|
applogger.L().Errorf("StatusCounts: %v", svcErr)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to get status counts")
|
|
return
|
|
}
|
|
|
|
response.OK(c, counts)
|
|
}
|
|
|
|
// reorderRequest is the JSON payload for the Reorder endpoint.
|
|
type reorderRequest struct {
|
|
Positions []positionEntry `json:"positions"`
|
|
}
|
|
|
|
// positionEntry maps an article ID to its new position value.
|
|
type positionEntry struct {
|
|
ID uint `json:"id"`
|
|
Position int `json:"position"`
|
|
}
|
|
|
|
// Reorder batch-updates article positions.
|
|
// POST /portals/:portal_id/articles/reorder
|
|
func (h *ArticleHandler) Reorder(c *gin.Context) {
|
|
portal, ok := h.resolvePortalForRequest(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
positions, err := bindArticleReorder(c)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := h.svc.ReorderScoped(c.Request.Context(), portal.ID, positions); err != nil {
|
|
applogger.L().Errorf("Reorder articles: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to reorder articles")
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusOK)
|
|
}
|
|
|
|
// bulkUpdateStatusRequest is the JSON payload for BulkUpdateStatus.
|
|
type bulkUpdateStatusRequest struct {
|
|
IDs []uint `json:"ids"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
// BulkUpdateStatus updates the status of multiple articles at once.
|
|
// POST /portals/:portal_id/articles/bulk_update_status
|
|
func (h *ArticleHandler) BulkUpdateStatus(c *gin.Context) {
|
|
portal, ok := h.resolvePortalForRequest(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req bulkUpdateStatusRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := h.svc.BulkUpdateStatusScoped(c.Request.Context(), portal.ID, req.IDs, req.Status); err != nil {
|
|
applogger.L().Errorf("BulkUpdateStatus articles: %v", err)
|
|
renderArticleBulkError(c, err)
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusOK)
|
|
}
|
|
|
|
type bulkUpdateCategoryRequest struct {
|
|
IDs []uint `json:"ids"`
|
|
CategoryID uint `json:"category_id"`
|
|
}
|
|
|
|
func (h *ArticleHandler) BulkUpdateCategory(c *gin.Context) {
|
|
portal, ok := h.resolvePortalForRequest(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req bulkUpdateCategoryRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
if err := h.svc.BulkUpdateCategoryScoped(c.Request.Context(), portal.ID, req.IDs, req.CategoryID); err != nil {
|
|
applogger.L().Errorf("BulkUpdateCategory articles: %v", err)
|
|
renderArticleBulkError(c, err)
|
|
return
|
|
}
|
|
c.Status(http.StatusOK)
|
|
}
|
|
|
|
// bulkDeleteRequest is the JSON payload for BulkDelete.
|
|
type bulkDeleteRequest struct {
|
|
IDs []uint `json:"ids"`
|
|
}
|
|
|
|
// BulkDelete deletes multiple articles at once.
|
|
// POST /portals/:portal_id/articles/bulk_delete
|
|
func (h *ArticleHandler) BulkDelete(c *gin.Context) {
|
|
portal, ok := h.resolvePortalForRequest(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req bulkDeleteRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
if err := h.svc.BulkDeleteScoped(c.Request.Context(), portal.ID, req.IDs); err != nil {
|
|
applogger.L().Errorf("BulkDelete articles: %v", err)
|
|
renderArticleBulkError(c, err)
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusOK)
|
|
}
|
|
|
|
func (h *ArticleHandler) BulkTranslate(c *gin.Context) {
|
|
c.Status(http.StatusNotImplemented)
|
|
}
|
|
|
|
// BulkActions performs a bulk operation (publish/archive/delete) on multiple articles.
|
|
// POST /api/v1/accounts/:account_id/portals/:portal_id/articles/bulk_actions
|
|
func (h *ArticleHandler) BulkActions(c *gin.Context) {
|
|
var req service.BulkActionsRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
if len(req.IDs) == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "ids must not be empty")
|
|
return
|
|
}
|
|
|
|
if err := h.svc.BulkActions(c.Request.Context(), &req); err != nil {
|
|
applogger.L().Errorf("BulkActions articles: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to perform bulk action")
|
|
return
|
|
}
|
|
|
|
response.OK(c, gin.H{"action": req.Action, "ids": req.IDs})
|
|
}
|
|
|
|
func (h *ArticleHandler) resolvePortalForRequest(c *gin.Context) (*model.Portal, bool) {
|
|
accountID := getAccountID(c)
|
|
if accountID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return nil, false
|
|
}
|
|
return h.resolvePortal(c, accountID)
|
|
}
|
|
|
|
func (h *ArticleHandler) resolvePortal(c *gin.Context, accountID uint) (*model.Portal, bool) {
|
|
if h.portalSvc != nil {
|
|
portal, err := h.portalSvc.ResolveByAccountAndRouteID(c.Request.Context(), accountID, c.Param("portal_id"))
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "portal not found")
|
|
return nil, false
|
|
}
|
|
return portal, true
|
|
}
|
|
portalID, err := strconv.ParseUint(c.Param("portal_id"), 10, 64)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid portal_id")
|
|
return nil, false
|
|
}
|
|
return &model.Portal{Base: model.Base{ID: uint(portalID)}, AccountID: accountID}, true
|
|
}
|
|
|
|
func (h *ArticleHandler) articleSearchParams(c *gin.Context, portalID uint, offset, limit int) (repository.ArticleSearchParams, bool) {
|
|
params := repository.ArticleSearchParams{
|
|
PortalID: portalID,
|
|
Query: c.Query("query"),
|
|
CategorySlug: c.Query("category_slug"),
|
|
Locale: c.Query("locale"),
|
|
Status: c.Query("status"),
|
|
SortBy: c.Query("sort_by"),
|
|
Offset: offset,
|
|
Limit: limit,
|
|
}
|
|
if params.SortBy == "" {
|
|
params.SortBy = c.Query("sort")
|
|
}
|
|
if authorIDStr := c.Query("author_id"); authorIDStr != "" {
|
|
aid, parseErr := strconv.ParseUint(authorIDStr, 10, 64)
|
|
if parseErr != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid author_id")
|
|
return params, false
|
|
}
|
|
aidUint := uint(aid)
|
|
params.AuthorID = &aidUint
|
|
}
|
|
return params, true
|
|
}
|
|
|
|
func articleIDParam(c *gin.Context) (uint, error) {
|
|
if id, err := parseUintAnyParam(c, "article_id", "id"); err == nil && id != 0 {
|
|
return id, nil
|
|
} else if err != nil {
|
|
return 0, err
|
|
}
|
|
return 0, strconv.ErrSyntax
|
|
}
|
|
|
|
func bindArticleReorder(c *gin.Context) (map[uint]int, error) {
|
|
var req struct {
|
|
PositionsHash map[string]int `json:"positions_hash"`
|
|
Positions []positionEntry `json:"positions"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
return nil, err
|
|
}
|
|
positions := make(map[uint]int, len(req.PositionsHash)+len(req.Positions))
|
|
for id, position := range req.PositionsHash {
|
|
parsed, err := strconv.ParseUint(id, 10, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
positions[uint(parsed)] = position
|
|
}
|
|
for _, entry := range req.Positions {
|
|
positions[entry.ID] = entry.Position
|
|
}
|
|
return positions, nil
|
|
}
|
|
|
|
func articlePayloads(articles []model.Article) []gin.H {
|
|
payload := make([]gin.H, 0, len(articles))
|
|
for i := range articles {
|
|
payload = append(payload, articlePayload(&articles[i]))
|
|
}
|
|
return payload
|
|
}
|
|
|
|
func articlePayload(article *model.Article) gin.H {
|
|
if article == nil {
|
|
return gin.H{}
|
|
}
|
|
payload := gin.H{
|
|
"id": article.ID,
|
|
"slug": article.Slug,
|
|
"title": article.Title,
|
|
"content": article.Content,
|
|
"description": article.Description,
|
|
"status": article.Status,
|
|
"position": article.Position,
|
|
"account_id": article.AccountID,
|
|
"updated_at": article.UpdatedAt.Unix(),
|
|
"meta": articleMetaPayload(article.Meta),
|
|
"category": articleCategoryPayload(article),
|
|
"views": article.Views,
|
|
"associated_articles": associatedArticlePayloads(article.AssociatedArticles),
|
|
}
|
|
if article.Author != nil && article.Author.ID != 0 {
|
|
payload["author"] = serializeAgentUser(article.Author, article.AccountID, article.Author.Role, "", false, 0)
|
|
}
|
|
return payload
|
|
}
|
|
|
|
func articleCategoryPayload(article *model.Article) gin.H {
|
|
payload := gin.H{"id": article.CategoryID, "name": nil, "slug": nil, "locale": nil}
|
|
if article.Category != nil {
|
|
payload["name"] = article.Category.Name
|
|
payload["slug"] = article.Category.Slug
|
|
payload["locale"] = article.Category.Locale
|
|
}
|
|
return payload
|
|
}
|
|
|
|
func associatedArticlePayloads(articles []model.Article) []gin.H {
|
|
payload := make([]gin.H, 0, len(articles))
|
|
for i := range articles {
|
|
article := &articles[i]
|
|
item := gin.H{
|
|
"id": article.ID,
|
|
"category_id": article.CategoryID,
|
|
"title": article.Title,
|
|
"content": article.Content,
|
|
"description": article.Description,
|
|
"status": article.Status,
|
|
"account_id": article.AccountID,
|
|
"views": article.Views,
|
|
}
|
|
if article.Portal.ID != 0 {
|
|
item["portal"] = portalPayload(&article.Portal, article.Locale, 0)
|
|
}
|
|
if article.Author != nil && article.Author.ID != 0 {
|
|
item["author"] = serializeAgentUser(article.Author, article.AccountID, article.Author.Role, "", false, 0)
|
|
}
|
|
payload = append(payload, item)
|
|
}
|
|
return payload
|
|
}
|
|
|
|
func articleListMetaPayload(meta *service.ArticleListMeta, currentPage int) gin.H {
|
|
return gin.H{
|
|
"all_articles_count": meta.AllArticlesCount,
|
|
"archived_articles_count": meta.ArchivedArticlesCount,
|
|
"articles_count": meta.ArticlesCount,
|
|
"current_page": currentPage,
|
|
"draft_articles_count": meta.DraftArticlesCount,
|
|
"mine_articles_count": meta.MineArticlesCount,
|
|
"published_count": meta.PublishedCount,
|
|
}
|
|
}
|
|
|
|
func articleMetaPayload(raw json.RawMessage) map[string]any {
|
|
if len(raw) == 0 {
|
|
return map[string]any{}
|
|
}
|
|
var obj map[string]any
|
|
if err := json.Unmarshal(raw, &obj); err != nil || obj == nil {
|
|
return map[string]any{}
|
|
}
|
|
return obj
|
|
}
|
|
|
|
func renderArticleBulkError(c *gin.Context, err error) {
|
|
message := "failed to update articles"
|
|
switch {
|
|
case errors.Is(err, service.ErrArticleBulkNoArticles):
|
|
message = "No articles found"
|
|
case errors.Is(err, service.ErrArticleBulkInvalidStatus):
|
|
message = "Invalid status"
|
|
case errors.Is(err, service.ErrArticleBulkCategoryNotFound):
|
|
message = "Category not found"
|
|
default:
|
|
message = err.Error()
|
|
}
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": message})
|
|
}
|