feat(help-center): expose public article lists
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -22,6 +23,55 @@ type ArticleHandler struct {
|
||||
portalSvc *service.PortalService
|
||||
}
|
||||
|
||||
// PublicList returns published help-center articles for the public portal/widget views.
|
||||
// GET /hc/:slug/:locale/articles.json
|
||||
func (h *ArticleHandler) PublicList(c *gin.Context) {
|
||||
portal, ok := h.resolvePublicPortal(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
page, perPage, offset, ok := publicArticlePagination(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
locale := c.Param("locale")
|
||||
query := strings.TrimSpace(c.Query("query"))
|
||||
params := repository.ArticleSearchParams{
|
||||
PortalID: portal.ID,
|
||||
Query: query,
|
||||
CategorySlug: c.Param("category_slug"),
|
||||
Locale: locale,
|
||||
Status: string(model.ArticleStatusPublished),
|
||||
SortBy: c.Query("sort"),
|
||||
Offset: offset,
|
||||
Limit: perPage,
|
||||
}
|
||||
|
||||
countParams := repository.ArticleSearchParams{
|
||||
PortalID: portal.ID,
|
||||
Locale: locale,
|
||||
Status: string(model.ArticleStatusPublished),
|
||||
}
|
||||
articlesCount, err := h.svc.Count(c.Request.Context(), countParams)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Public article count: %v", err)
|
||||
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to list articles")
|
||||
return
|
||||
}
|
||||
articles, _, err := h.svc.Search(c.Request.Context(), params)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Public article list: %v", err)
|
||||
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to list articles")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"payload": publicArticlePayloads(articles, portal.Slug),
|
||||
"meta": gin.H{"articles_count": articlesCount, "current_page": page},
|
||||
})
|
||||
}
|
||||
|
||||
// NewArticleHandler creates a new ArticleHandler.
|
||||
func NewArticleHandler(svc *service.ArticleService, portalSvc ...*service.PortalService) *ArticleHandler {
|
||||
h := &ArticleHandler{svc: svc}
|
||||
@@ -456,6 +506,53 @@ func (h *ArticleHandler) resolvePortal(c *gin.Context, accountID uint) (*model.P
|
||||
return &model.Portal{Base: model.Base{ID: uint(portalID)}, AccountID: accountID}, true
|
||||
}
|
||||
|
||||
func (h *ArticleHandler) 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 publicArticlePagination(c *gin.Context) (int, int, int, bool) {
|
||||
page := 1
|
||||
if raw := c.Query("page"); raw != "" {
|
||||
parsed, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid page")
|
||||
return 0, 0, 0, false
|
||||
}
|
||||
if parsed > 0 {
|
||||
page = parsed
|
||||
}
|
||||
}
|
||||
perPage := 0
|
||||
if raw := c.Query("per_page"); raw != "" {
|
||||
parsed, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid per_page")
|
||||
return 0, 0, 0, false
|
||||
}
|
||||
perPage = parsed
|
||||
if perPage < 1 {
|
||||
perPage = 25
|
||||
}
|
||||
if perPage > 100 {
|
||||
perPage = 100
|
||||
}
|
||||
}
|
||||
offset := 0
|
||||
if perPage > 0 {
|
||||
offset = (page - 1) * perPage
|
||||
}
|
||||
return page, perPage, offset, true
|
||||
}
|
||||
|
||||
func (h *ArticleHandler) articleSearchParams(c *gin.Context, portalID uint, offset, limit int) (repository.ArticleSearchParams, bool) {
|
||||
params := repository.ArticleSearchParams{
|
||||
PortalID: portalID,
|
||||
@@ -581,6 +678,96 @@ func associatedArticlePayloads(articles []model.Article) []gin.H {
|
||||
return payload
|
||||
}
|
||||
|
||||
func publicArticlePayloads(articles []model.Article, portalSlug string) []gin.H {
|
||||
payload := make([]gin.H, 0, len(articles))
|
||||
for i := range articles {
|
||||
payload = append(payload, publicArticlePayload(&articles[i], portalSlug))
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func publicArticlePayload(article *model.Article, portalSlug string) gin.H {
|
||||
if article == nil {
|
||||
return gin.H{}
|
||||
}
|
||||
if article.Portal.Slug != "" {
|
||||
portalSlug = article.Portal.Slug
|
||||
}
|
||||
payload := gin.H{
|
||||
"id": article.ID,
|
||||
"category_id": article.CategoryID,
|
||||
"title": article.Title,
|
||||
"content": article.Content,
|
||||
"description": article.Description,
|
||||
"status": article.Status,
|
||||
"position": article.Position,
|
||||
"account_id": article.AccountID,
|
||||
"last_updated_at": article.UpdatedAt,
|
||||
"slug": article.Slug,
|
||||
"views": article.Views,
|
||||
"associated_articles": publicAssociatedArticlePayloads(article.AssociatedArticles),
|
||||
"link": "hc/" + portalSlug + "/articles/" + article.Slug,
|
||||
}
|
||||
if article.Portal.ID != 0 {
|
||||
payload["portal"] = publicPortalPayload(&article.Portal)
|
||||
}
|
||||
if article.Category != nil && article.Category.ID != 0 {
|
||||
payload["category"] = gin.H{"id": article.Category.ID, "slug": article.Category.Slug, "locale": article.Category.Locale}
|
||||
}
|
||||
if article.Author != nil && article.Author.ID != 0 {
|
||||
payload["author"] = publicAuthorPayload(article.Author)
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func publicAssociatedArticlePayloads(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,
|
||||
"last_updated_at": article.UpdatedAt,
|
||||
"views": article.Views,
|
||||
}
|
||||
if article.Author != nil && article.Author.ID != 0 {
|
||||
item["author"] = publicAuthorPayload(article.Author)
|
||||
}
|
||||
payload = append(payload, item)
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func publicPortalPayload(portal *model.Portal) gin.H {
|
||||
if portal == nil {
|
||||
return gin.H{}
|
||||
}
|
||||
return gin.H{
|
||||
"custom_domain": portal.CustomDomain,
|
||||
"header_text": portal.HeaderText,
|
||||
"homepage_link": portal.HomepageLink,
|
||||
"name": portal.Name,
|
||||
"page_title": portal.PageTitle,
|
||||
"slug": portal.Slug,
|
||||
}
|
||||
}
|
||||
|
||||
func publicAuthorPayload(user *model.User) gin.H {
|
||||
if user == nil {
|
||||
return gin.H{}
|
||||
}
|
||||
availableName := user.DisplayName
|
||||
if availableName == "" {
|
||||
availableName = user.Name
|
||||
}
|
||||
return gin.H{"available_name": availableName, "name": user.Name, "thumbnail": user.AvatarURL}
|
||||
}
|
||||
|
||||
func articleListMetaPayload(meta *service.ArticleListMeta, currentPage int) gin.H {
|
||||
return gin.H{
|
||||
"all_articles_count": meta.AllArticlesCount,
|
||||
|
||||
Reference in New Issue
Block a user