feat: update article and stats

This commit is contained in:
hanif salafi 2025-02-15 08:56:13 +07:00
parent 62223041c1
commit f8a6136b95
15 changed files with 268 additions and 21 deletions

View File

@ -6,7 +6,7 @@ type ActivityLogs struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"` ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
ActivityTypeId int `json:"activity_type_id" gorm:"type:int4"` ActivityTypeId int `json:"activity_type_id" gorm:"type:int4"`
Url string `json:"url" gorm:"type:varchar"` Url string `json:"url" gorm:"type:varchar"`
ArticleId *int `json:"article_id" gorm:"type:int4"` ArticleId *uint `json:"article_id" gorm:"type:int4"`
UserId *uint `json:"user_id" gorm:"type:int4"` UserId *uint `json:"user_id" gorm:"type:int4"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"` CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
} }

View File

@ -15,10 +15,10 @@ type Articles struct {
ThumbnailPath *string `json:"thumbnail_path" gorm:"type:varchar"` ThumbnailPath *string `json:"thumbnail_path" gorm:"type:varchar"`
PageUrl *string `json:"page_url" gorm:"type:varchar"` PageUrl *string `json:"page_url" gorm:"type:varchar"`
CreatedById *uint `json:"created_by_id" gorm:"type:int4"` CreatedById *uint `json:"created_by_id" gorm:"type:int4"`
AiArticleId *int `json:"ai_article_id" gorm:"type:int4"`
CommentCount *int `json:"comment_count" gorm:"type:int4"`
ShareCount *int `json:"share_count" gorm:"type:int4"` ShareCount *int `json:"share_count" gorm:"type:int4"`
ViewCount *int `json:"view_count" gorm:"type:int4"` ViewCount *int `json:"view_count" gorm:"type:int4"`
AiArticleId *int `json:"ai_article_id" gorm:"type:int4"`
DownloadCount *int `json:"download_count" gorm:"type:int4"`
StatusId *int `json:"status_id" gorm:"type:int4"` StatusId *int `json:"status_id" gorm:"type:int4"`
OldId *uint `json:"old_id" gorm:"type:int4"` OldId *uint `json:"old_id" gorm:"type:int4"`
IsPublish *bool `json:"is_publish" gorm:"type:bool;default:false"` IsPublish *bool `json:"is_publish" gorm:"type:bool;default:false"`

View File

@ -25,11 +25,6 @@ var activityLogTypes = []entity.ActivityLogTypes{
}, },
{ {
ID: 4, ID: 4,
Name: "Like",
IsActive: true,
},
{
ID: 5,
Name: "Comment", Name: "Comment",
IsActive: true, IsActive: true,
}, },

View File

@ -22,7 +22,7 @@ type ActivityLogsQueryRequest struct {
type ActivityLogsCreateRequest struct { type ActivityLogsCreateRequest struct {
ActivityTypeId int `json:"activityTypeId" validate:"required"` ActivityTypeId int `json:"activityTypeId" validate:"required"`
Url string `json:"url" validate:"required"` Url string `json:"url" validate:"required"`
ArticleId *int `json:"articleId"` ArticleId *uint `json:"articleId"`
UserId *uint `json:"userId"` UserId *uint `json:"userId"`
} }
@ -40,7 +40,7 @@ type ActivityLogsUpdateRequest struct {
ID uint `json:"id" validate:"required"` ID uint `json:"id" validate:"required"`
ActivityTypeId int `json:"activityTypeId" validate:"required"` ActivityTypeId int `json:"activityTypeId" validate:"required"`
Url string `json:"url" validate:"required"` Url string `json:"url" validate:"required"`
ArticleId *int `json:"articleId"` ArticleId *uint `json:"articleId"`
UserId *uint `json:"userId"` UserId *uint `json:"userId"`
} }

View File

@ -6,7 +6,7 @@ type ActivityLogsResponse struct {
ID uint `json:"id"` ID uint `json:"id"`
ActivityTypeId int `json:"activityTypeId"` ActivityTypeId int `json:"activityTypeId"`
Url string `json:"url"` Url string `json:"url"`
ArticleId *int `json:"articleId"` ArticleId *uint `json:"articleId"`
UserId *uint `json:"userId"` UserId *uint `json:"userId"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
} }

View File

@ -7,6 +7,7 @@ import (
"go-humas-be/app/module/activity_logs/repository" "go-humas-be/app/module/activity_logs/repository"
"go-humas-be/app/module/activity_logs/request" "go-humas-be/app/module/activity_logs/request"
"go-humas-be/app/module/activity_logs/response" "go-humas-be/app/module/activity_logs/response"
"go-humas-be/app/module/articles/service"
usersRepository "go-humas-be/app/module/users/repository" usersRepository "go-humas-be/app/module/users/repository"
"go-humas-be/utils/paginator" "go-humas-be/utils/paginator"
utilSvc "go-humas-be/utils/service" utilSvc "go-humas-be/utils/service"
@ -14,9 +15,10 @@ import (
// ActivityLogsService // ActivityLogsService
type activityLogsService struct { type activityLogsService struct {
Repo repository.ActivityLogsRepository Repo repository.ActivityLogsRepository
UsersRepo usersRepository.UsersRepository UsersRepo usersRepository.UsersRepository
Log zerolog.Logger ArticleService service.ArticlesService
Log zerolog.Logger
} }
// ActivityLogsService define interface of IActivityLogsService // ActivityLogsService define interface of IActivityLogsService
@ -29,12 +31,13 @@ type ActivityLogsService interface {
} }
// NewActivityLogsService init ActivityLogsService // NewActivityLogsService init ActivityLogsService
func NewActivityLogsService(repo repository.ActivityLogsRepository, log zerolog.Logger, usersRepo usersRepository.UsersRepository) ActivityLogsService { func NewActivityLogsService(repo repository.ActivityLogsRepository, log zerolog.Logger, usersRepo usersRepository.UsersRepository, articleService service.ArticlesService) ActivityLogsService {
return &activityLogsService{ return &activityLogsService{
Repo: repo, Repo: repo,
Log: log, Log: log,
UsersRepo: usersRepo, UsersRepo: usersRepo,
ArticleService: articleService,
} }
} }
@ -71,7 +74,18 @@ func (_i *activityLogsService) Save(req request.ActivityLogsCreateRequest, authT
newReq.UserId = &createdBy.ID newReq.UserId = &createdBy.ID
} }
return _i.Repo.Create(newReq) result, err := _i.Repo.Create(newReq)
if err != nil {
return nil, err
}
// update article
err = _i.ArticleService.UpdateActivityCount(*req.ArticleId, req.ActivityTypeId)
if err != nil {
return nil, err
}
return result, nil
} }
func (_i *activityLogsService) Update(id uint, req request.ActivityLogsUpdateRequest) (err error) { func (_i *activityLogsService) Update(id uint, req request.ActivityLogsUpdateRequest) (err error) {

View File

@ -51,5 +51,6 @@ func (_i *ArticlesRouter) RegisterArticlesRoutes() {
router.Post("/thumbnail/:id", articlesController.SaveThumbnail) router.Post("/thumbnail/:id", articlesController.SaveThumbnail)
router.Get("/thumbnail/viewer/:thumbnailName", articlesController.Viewer) router.Get("/thumbnail/viewer/:thumbnailName", articlesController.Viewer)
router.Delete("/:id", articlesController.Delete) router.Delete("/:id", articlesController.Delete)
router.Get("/statistic/summary", articlesController.SummaryStats)
}) })
} }

View File

@ -23,6 +23,7 @@ type ArticlesController interface {
Update(c *fiber.Ctx) error Update(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error Delete(c *fiber.Ctx) error
Viewer(c *fiber.Ctx) error Viewer(c *fiber.Ctx) error
SummaryStats(c *fiber.Ctx) error
} }
func NewArticlesController(articlesService service.ArticlesService) ArticlesController { func NewArticlesController(articlesService service.ArticlesService) ArticlesController {
@ -237,3 +238,29 @@ func (_i *articlesController) Delete(c *fiber.Ctx) error {
func (_i *articlesController) Viewer(c *fiber.Ctx) error { func (_i *articlesController) Viewer(c *fiber.Ctx) error {
return _i.articlesService.Viewer(c) return _i.articlesService.Viewer(c)
} }
// SummaryStats Articles
// @Summary SummaryStats Articles
// @Description API for Summary Stats of Article
// @Tags Articles
// @Security Bearer
// @Param Authorization header string true "Insert your access token" default(Bearer <Add access token here>)
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /articles/statistic/summary [get]
func (_i *articlesController) SummaryStats(c *fiber.Ctx) error {
authToken := c.Get("Authorization")
response, err := _i.articlesService.SummaryStats(authToken)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Summary Stats of Articles successfully retrieved"},
Data: response,
})
}

View File

@ -67,7 +67,7 @@ func ArticlesResponseMapper(
CreatedByName: &createdByName, CreatedByName: &createdByName,
ShareCount: articlesReq.ShareCount, ShareCount: articlesReq.ShareCount,
ViewCount: articlesReq.ViewCount, ViewCount: articlesReq.ViewCount,
DownloadCount: articlesReq.DownloadCount, CommentCount: articlesReq.CommentCount,
StatusId: articlesReq.StatusId, StatusId: articlesReq.StatusId,
IsPublish: articlesReq.IsPublish, IsPublish: articlesReq.IsPublish,
PublishedAt: articlesReq.PublishedAt, PublishedAt: articlesReq.PublishedAt,

View File

@ -6,8 +6,10 @@ import (
"go-humas-be/app/database" "go-humas-be/app/database"
"go-humas-be/app/database/entity" "go-humas-be/app/database/entity"
"go-humas-be/app/module/articles/request" "go-humas-be/app/module/articles/request"
"go-humas-be/app/module/articles/response"
"go-humas-be/utils/paginator" "go-humas-be/utils/paginator"
"strings" "strings"
"time"
) )
type articlesRepository struct { type articlesRepository struct {
@ -23,6 +25,7 @@ type ArticlesRepository interface {
Create(articles *entity.Articles) (articleReturn *entity.Articles, err error) Create(articles *entity.Articles) (articleReturn *entity.Articles, err error)
Update(id uint, articles *entity.Articles) (err error) Update(id uint, articles *entity.Articles) (err error)
Delete(id uint) (err error) Delete(id uint) (err error)
SummaryStats(userID uint) (articleSummaryStats *response.ArticleSummaryStats, err error)
} }
func NewArticlesRepository(db *database.Database, log zerolog.Logger) ArticlesRepository { func NewArticlesRepository(db *database.Database, log zerolog.Logger) ArticlesRepository {
@ -128,3 +131,25 @@ func (_i *articlesRepository) Update(id uint, articles *entity.Articles) (err er
func (_i *articlesRepository) Delete(id uint) error { func (_i *articlesRepository) Delete(id uint) error {
return _i.DB.DB.Delete(&entity.Articles{}, id).Error return _i.DB.DB.Delete(&entity.Articles{}, id).Error
} }
func (_i *articlesRepository) SummaryStats(userID uint) (articleSummaryStats *response.ArticleSummaryStats, err error) {
now := time.Now()
startOfDay := now.Truncate(24 * time.Hour)
startOfWeek := now.AddDate(0, 0, -int(now.Weekday())+1).Truncate(24 * time.Hour)
// Query
err = _i.DB.DB.Model(&entity.Articles{}).
Select(
"COUNT(*) AS total_all, "+
"COALESCE(SUM(view_count), 0) AS total_views, "+
"COALESCE(SUM(share_count), 0) AS total_shares, "+
"COALESCE(SUM(comment_count), 0) AS total_comments, "+
"COUNT(CASE WHEN created_at >= ? THEN 1 END) AS total_today, "+
"COUNT(CASE WHEN created_at >= ? THEN 1 END) AS total_this_week",
startOfDay, startOfWeek,
).
Where("created_by_id = ?", userID).
Scan(&articleSummaryStats).Error
return articleSummaryStats, err
}

View File

@ -22,7 +22,7 @@ type ArticlesResponse struct {
CreatedByName *string `json:"createdByName"` CreatedByName *string `json:"createdByName"`
ShareCount *int `json:"shareCount"` ShareCount *int `json:"shareCount"`
ViewCount *int `json:"viewCount"` ViewCount *int `json:"viewCount"`
DownloadCount *int `json:"downloadCount"` CommentCount *int `json:"commentCount"`
AiArticleId *int `json:"aiArticleId"` AiArticleId *int `json:"aiArticleId"`
StatusId *int `json:"statusId"` StatusId *int `json:"statusId"`
IsPublish *bool `json:"isPublish"` IsPublish *bool `json:"isPublish"`
@ -34,3 +34,12 @@ type ArticlesResponse struct {
ArticleFiles []*articleFilesResponse.ArticleFilesResponse `json:"files"` ArticleFiles []*articleFilesResponse.ArticleFilesResponse `json:"files"`
ArticleCategories []*articleCategoriesResponse.ArticleCategoriesResponse `json:"categories"` ArticleCategories []*articleCategoriesResponse.ArticleCategoriesResponse `json:"categories"`
} }
type ArticleSummaryStats struct {
TotalToday int `json:"totalToday"`
TotalThisWeek int `json:"totalThisWeek"`
TotalAll int `json:"totalAll"`
TotalViews int `json:"totalViews"`
TotalShares int `json:"totalShares"`
TotalComments int `json:"totalComments"`
}

View File

@ -51,7 +51,9 @@ type ArticlesService interface {
SaveThumbnail(c *fiber.Ctx) (err error) SaveThumbnail(c *fiber.Ctx) (err error)
Update(id uint, req request.ArticlesUpdateRequest) (err error) Update(id uint, req request.ArticlesUpdateRequest) (err error)
Delete(id uint) error Delete(id uint) error
UpdateActivityCount(id uint, activityTypeId int) (err error)
Viewer(c *fiber.Ctx) error Viewer(c *fiber.Ctx) error
SummaryStats(authToken string) (summaryStats *response.ArticleSummaryStats, err error)
} }
// NewArticlesService init ArticlesService // NewArticlesService init ArticlesService
@ -356,6 +358,48 @@ func (_i *articlesService) Viewer(c *fiber.Ctx) (err error) {
return return
} }
func (_i *articlesService) UpdateActivityCount(id uint, activityTypeId int) error {
result, err := _i.Repo.FindOne(id)
if err != nil {
return err
}
viewCount := 0
if result.ViewCount != nil {
viewCount = *result.ViewCount
}
shareCount := 0
if result.ShareCount != nil {
shareCount = *result.ShareCount
}
commentCount := 0
if result.CommentCount != nil {
commentCount = *result.CommentCount
}
if activityTypeId == 2 {
viewCount++
} else if activityTypeId == 3 {
shareCount++
} else if activityTypeId == 4 {
commentCount++
}
result.ViewCount = &viewCount
result.ShareCount = &shareCount
result.CommentCount = &commentCount
return _i.Repo.Update(id, result)
}
func (_i *articlesService) SummaryStats(authToken string) (summaryStats *response.ArticleSummaryStats, err error) {
user := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
result, err := _i.Repo.SummaryStats(user.ID)
if err != nil {
return nil, err
}
return result, nil
}
func getFileExtension(filename string) string { func getFileExtension(filename string) string {
// split file name // split file name
parts := strings.Split(filename, ".") parts := strings.Split(filename, ".")

View File

@ -2339,6 +2339,56 @@ const docTemplate = `{
} }
} }
}, },
"/articles/statistic/summary": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "API for Summary Stats of Article",
"tags": [
"Articles"
],
"summary": "SummaryStats Articles",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cAdd access token here\u003e",
"description": "Insert your access token",
"name": "Authorization",
"in": "header",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.BadRequestError"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.UnauthorizedError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.InternalServerError"
}
}
}
}
},
"/articles/thumbnail/viewer/{thumbnailName}": { "/articles/thumbnail/viewer/{thumbnailName}": {
"get": { "get": {
"security": [ "security": [

View File

@ -2328,6 +2328,56 @@
} }
} }
}, },
"/articles/statistic/summary": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "API for Summary Stats of Article",
"tags": [
"Articles"
],
"summary": "SummaryStats Articles",
"parameters": [
{
"type": "string",
"default": "Bearer \u003cAdd access token here\u003e",
"description": "Insert your access token",
"name": "Authorization",
"in": "header",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.Response"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.BadRequestError"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.UnauthorizedError"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.InternalServerError"
}
}
}
}
},
"/articles/thumbnail/viewer/{thumbnailName}": { "/articles/thumbnail/viewer/{thumbnailName}": {
"get": { "get": {
"security": [ "security": [

View File

@ -2354,6 +2354,38 @@ paths:
summary: Update Articles summary: Update Articles
tags: tags:
- Articles - Articles
/articles/statistic/summary:
get:
description: API for Summary Stats of Article
parameters:
- default: Bearer <Add access token here>
description: Insert your access token
in: header
name: Authorization
required: true
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.Response'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.BadRequestError'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.UnauthorizedError'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.InternalServerError'
security:
- Bearer: []
summary: SummaryStats Articles
tags:
- Articles
/articles/thumbnail/{id}: /articles/thumbnail/{id}:
post: post:
description: API for Save Thumbnail of Articles description: API for Save Thumbnail of Articles