feat(help-center): expose public category lists

This commit is contained in:
2026-06-06 07:23:55 +08:00
parent 53434ab397
commit c64b4c2f92
10 changed files with 179 additions and 10 deletions
@@ -3,6 +3,7 @@ package v1
import (
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/model"
@@ -25,6 +26,41 @@ func NewCategoryHandler(svc *service.CategoryService, portalSvc ...*service.Port
return h
}
// PublicList returns public help-center categories for a portal.
// GET /hc/:slug/:locale/categories.json
func (h *CategoryHandler) PublicList(c *gin.Context) {
portal, ok := h.resolvePublicPortal(c)
if !ok {
return
}
categories, _, err := h.svc.ListByPortalID(c.Request.Context(), portal.ID, "", 0, 0)
if err != nil {
applogger.L().Errorf("Public list categories: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to list categories")
return
}
payload := make([]gin.H, 0, len(categories))
for i := range categories {
payload = append(payload, publicCategoryPayload(&categories[i]))
}
c.JSON(http.StatusOK, gin.H{"payload": payload})
}
// PublicGet returns a public help-center category by slug and locale.
// GET /hc/:slug/:locale/categories/:category_slug.json
func (h *CategoryHandler) PublicGet(c *gin.Context) {
portal, ok := h.resolvePublicPortal(c)
if !ok {
return
}
category, err := h.svc.GetByPortalSlugAndLocale(c.Request.Context(), portal.ID, publicCategorySlugParam(c), c.Param("locale"))
if err != nil {
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "category not found")
return
}
c.JSON(http.StatusOK, publicCategoryPayload(category))
}
// Create creates a new category.
// POST /api/v1/accounts/:account_id/portals/:portal_id/categories
func (h *CategoryHandler) Create(c *gin.Context) {
@@ -216,6 +252,19 @@ func (h *CategoryHandler) resolvePortal(c *gin.Context, accountID uint) (*model.
return &model.Portal{Base: model.Base{ID: uint(portalID)}, AccountID: accountID}, true
}
func (h *CategoryHandler) resolvePublicPortal(c *gin.Context) (*model.Portal, bool) {
if h.portalSvc == nil {
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "portal not found")
return nil, false
}
portal, err := h.portalSvc.ResolvePublicBySlug(c.Request.Context(), c.Param("slug"))
if err != nil {
response.AbortWithStatusError(c, http.StatusNotFound, response.ErrNotFound, "portal not found")
return nil, false
}
return portal, true
}
func categoryPayload(category *model.Category, currentLocale string) gin.H {
if category == nil {
return gin.H{}
@@ -239,6 +288,20 @@ func categoryPayload(category *model.Category, currentLocale string) gin.H {
return payload
}
func publicCategoryPayload(category *model.Category) gin.H {
if category == nil {
return gin.H{}
}
return gin.H{
"name": category.Name,
"slug": category.Slug,
"locale": category.Locale,
"description": category.Description,
"position": category.Position,
"meta": gin.H{"articles_count": categoryPublishedArticleCount(category.Articles)},
}
}
func relatedCategoryPayloads(related []model.RelatedCategory) []gin.H {
payload := make([]gin.H, 0, len(related))
for _, item := range related {
@@ -271,6 +334,23 @@ func categoryArticleCount(articles []model.Article, locale string) int {
return count
}
func categoryPublishedArticleCount(articles []model.Article) int {
count := 0
for _, article := range articles {
if article.Status == string(model.ArticleStatusPublished) {
count++
}
}
return count
}
func publicCategorySlugParam(c *gin.Context) string {
if slug := c.Param("category_slug"); slug != "" {
return strings.TrimSuffix(slug, ".json")
}
return strings.TrimSuffix(c.Param("category_slug.json"), ".json")
}
func bindCategoryReorder(c *gin.Context) (map[uint]int, error) {
var req struct {
PositionsHash map[string]int `json:"positions_hash"`