Compare commits

..

No commits in common. "main" and "old-all-module" have entirely different histories.

260 changed files with 58907 additions and 13099 deletions

View File

@ -1,43 +0,0 @@
kind: pipeline
type: ssh
name: campaign-pool-be-build-deploy
server:
host:
from_secret: ssh_host
user:
from_secret: ssh_user
ssh_key:
from_secret: ssh_key
steps:
- name: prepare repo
when:
branch:
- main
commands:
- rm -rf /opt/build/campaignpool-be
- mkdir -p /opt/build/campaignpool-be
- cd /opt/build/campaignpool-be
- git clone http://38.47.180.165:3000/campaignpool/campaignpool-be.git
- name: build image
when:
branch:
- main
commands:
- docker login 38.47.180.165:3000 -u administrator -p HarborDockerImageRep0
- cd /opt/build/campaignpool-be/campaignpool-be
- docker build -t 38.47.180.165:3000/campaignpool/campaignpool-be:main .
- docker push 38.47.180.165:3000/campaignpool/campaignpool-be:main
- name: deploy
when:
branch:
- main
commands:
- docker login 38.47.180.165:3000 -u administrator -p HarborDockerImageRep0
- docker pull 38.47.180.165:3000/campaignpool/campaignpool-be:main
- docker stop campaignpool-be || true
- docker rm campaignpool-be || true
- docker run -dt -p 8807:8800 --name campaignpool-be 38.47.180.165:3000/campaignpool/campaignpool-be:main

View File

@ -16,12 +16,12 @@ build-2:
image: docker/compose:latest
services:
- name: docker:dind
command: ["--insecure-registry=38.47.185.86:8900"]
command: [ "--insecure-registry=103.82.242.92:8900" ]
script:
- docker login -u $DEPLOY_USERNAME -p $DEPLOY_TOKEN 38.47.185.86:8900
- docker login -u $DEPLOY_USERNAME -p $DEPLOY_TOKEN 103.82.242.92:8900
- docker-compose build
- docker tag registry.gitlab.com/hanifsalafi/campaign-pool-be:dev 38.47.185.86:8900/medols/campaign-pool-be:dev
- docker push 38.47.185.86:8900/medols/campaign-pool-be:dev
- docker tag registry.gitlab.com/hanifsalafi/campaign-pool-be:dev 103.82.242.92:8900/medols/campaign-pool-be:dev
- docker push 103.82.242.92:8900/medols/campaign-pool-be:dev
deploy:
stage: deploy
@ -30,4 +30,4 @@ deploy:
services:
- docker:dind
script:
- curl --user $JENKINS_USER:$JENKINS_PWD http://38.47.185.86:8080/job/autodeploy-campaignpool-be/build?token=autodeploycampaignpool
- curl --user $JENKINS_USER:$JENKINS_PWD http://38.47.180.165:8080/job/autodeploy-campaignpool-be/build?token=autodeploycampaignpool

View File

@ -0,0 +1,20 @@
package entity
import (
"time"
)
type Advertisement struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Title string `json:"title" gorm:"type:varchar"`
Description string `json:"description" gorm:"type:varchar"`
RedirectLink string `json:"redirect_link" gorm:"type:varchar"`
ContentFilePath *string `json:"content_file_path" gorm:"type:varchar"`
ContentFileName *string `json:"content_file_name" gorm:"type:varchar"`
Placement string `json:"placement" gorm:"type:varchar"`
StatusId int `json:"status_id" gorm:"type:int4"`
IsPublish bool `json:"is_publish" gorm:"type:bool"`
IsActive bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,19 @@
package entity
import (
"campaign-pool-be/app/database/entity/users"
"time"
)
type AIChatLogs struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
SessionID uint `json:"session_id" gorm:"type:int4;not null;index"`
UserID uint `json:"user_id" gorm:"type:int4;not null;index"`
StartDate time.Time `json:"start_date" gorm:"not null"`
EndDate *time.Time `json:"end_date"`
TotalDuration int64 `json:"total_duration" gorm:"type:bigint;default:0"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
User *users.Users `json:"user" gorm:"foreignKey:UserID;references:ID"`
}

View File

@ -0,0 +1,14 @@
package entity
import (
"time"
)
type AIChatMessages struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
SessionID string `json:"session_id" gorm:"type:varchar;not null;index"`
MessageType string `json:"message_type" gorm:"type:varchar;not null"`
Content string `json:"content" gorm:"type:text;not null"`
IsActive bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,19 @@
package entity
import (
"campaign-pool-be/app/database/entity/users"
"time"
)
type AIChatSessions struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
SessionID string `json:"session_id" gorm:"type:varchar;not null;unique;index"`
UserID uint `json:"user_id" gorm:"type:int4;not null;index"`
AgentID string `json:"agent_id" gorm:"type:varchar;not null"`
Title string `json:"title" gorm:"type:varchar;not null"`
MessageCount int `json:"message_count" gorm:"type:int4;default:0"`
IsActive bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
User *users.Users `json:"user" gorm:"foreignKey:UserID;references:ID"`
}

View File

@ -0,0 +1,15 @@
package entity
import (
"time"
)
type ArticleApprovals struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
ArticleId uint `json:"article_id" gorm:"type:int4"`
ApprovalBy uint `json:"approval_by" gorm:"type:int4"`
StatusId int `json:"status_id" gorm:"type:int4"`
Message string `json:"message" gorm:"type:varchar"`
ApprovalAtLevel *int `json:"approval_at_level" gorm:"type:int4"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,24 @@
package entity
import (
"time"
)
type ArticleCategories struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Title string `json:"title" gorm:"type:varchar"`
Description string `json:"description" gorm:"type:varchar"`
ThumbnailPath *string `json:"thumbnail_path" gorm:"type:varchar"`
Slug *string `json:"slug" gorm:"type:varchar"`
ParentId *int `json:"parent_id" gorm:"type:int4"`
Tags *string `json:"tags" gorm:"type:varchar"`
Position *int `json:"position" gorm:"type:int4"`
CreatedById *uint `json:"created_by_id" gorm:"type:int4"`
OldCategoryId *uint `json:"old_category_id" gorm:"type:int4"`
StatusId int `json:"status_id" gorm:"type:int4;default:1"`
IsPublish *bool `json:"is_publish" gorm:"type:bool;default:false"`
PublishedAt *time.Time `json:"published_at" gorm:"type:timestamp"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,16 @@
package article_category_details
import (
entity "campaign-pool-be/app/database/entity"
"time"
)
type ArticleCategoryDetails struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
ArticleId uint `json:"article_id" gorm:"type:int4"`
CategoryId int `json:"category_id" gorm:"type:int4"`
Category *entity.ArticleCategories `json:"category" gorm:"foreignKey:CategoryId;references:ID"`
IsActive bool `json:"is_active" gorm:"type:bool"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,21 @@
package entity
import (
"time"
)
type ArticleComments struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Message string `json:"message" gorm:"type:varchar"`
ArticleId uint `json:"article_id" gorm:"type:int4"`
CommentFrom *uint `json:"comment_from" gorm:"type:int4"`
ParentId *int `json:"parent_id" gorm:"type:int4"`
IsPublic bool `json:"is_public" gorm:"type:bool;default:false"`
StatusId int `json:"status_id" gorm:"type:int4;default:0"`
ApprovedAt *time.Time `json:"approved_at" gorm:"type:timestamp"`
IsActive bool `json:"is_active" gorm:"type:bool"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}
// statusId => 0: waiting, 1: accepted, 2: replied, 3: rejected

View File

@ -0,0 +1,27 @@
package entity
import (
"time"
)
type ArticleFiles struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
ArticleId uint `json:"article_id" gorm:"type:int4"`
UploadID *string `json:"upload_id" gorm:"type:varchar"`
FilePath *string `json:"file_path" gorm:"type:varchar"`
FileUrl *string `json:"file_url" gorm:"type:varchar"`
FileName *string `json:"file_name" gorm:"type:varchar"`
FileThumbnail *string `json:"file_thumbnail" gorm:"type:varchar"`
FileAlt *string `json:"file_alt" gorm:"type:varchar"`
WidthPixel *string `json:"width_pixel" gorm:"type:varchar"`
HeightPixel *string `json:"height_pixel" gorm:"type:varchar"`
Size *string `json:"size" gorm:"type:varchar"`
DownloadCount *int `json:"download_count" gorm:"type:int4;default:0"`
CreatedById int `json:"created_by_id" gorm:"type:int4"`
StatusId int `json:"status_id" gorm:"type:int4"`
IsPublish *bool `json:"is_publish" gorm:"type:bool;default:false"`
PublishedAt *time.Time `json:"published_at" gorm:"type:timestamp"`
IsActive bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,37 @@
package entity
import (
"time"
)
type Articles struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Title string `json:"title" gorm:"type:varchar"`
Slug string `json:"slug" gorm:"type:varchar"`
Description string `json:"description" gorm:"type:varchar"`
CategoryId int `json:"category_id" gorm:"type:int4"`
HtmlDescription string `json:"html_description" gorm:"type:varchar"`
TypeId int `json:"type_id" gorm:"type:int4"`
Tags string `json:"tags" gorm:"type:varchar"`
ThumbnailName *string `json:"thumbnail_name" gorm:"type:varchar"`
ThumbnailPath *string `json:"thumbnail_path" gorm:"type:varchar"`
PageUrl *string `json:"page_url" gorm:"type:varchar"`
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;default:0"`
ShareCount *int `json:"share_count" gorm:"type:int4;default:0"`
ViewCount *int `json:"view_count" gorm:"type:int4;default:0"`
StatusId *int `json:"status_id" gorm:"type:int4"`
OldId *uint `json:"old_id" gorm:"type:int4"`
NeedApprovalFrom *int `json:"need_approval_from" gorm:"type:int4"`
HasApprovedBy *string `json:"has_approved_by" gorm:"type:varchar"`
IsPublish *bool `json:"is_publish" gorm:"type:bool;default:false"`
IsBanner *bool `json:"is_banner" gorm:"type:bool;default:false"`
PublishedAt *time.Time `json:"published_at" gorm:"type:timestamp"`
IsDraft *bool `json:"is_draft" gorm:"type:bool;default:false"`
DraftedAt *time.Time `json:"drafted_at" gorm:"type:timestamp"`
PublishSchedule *string `json:"publish_schedule" gorm:"type:varchar"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,24 @@
package entity
import (
"campaign-pool-be/app/database/entity/users"
"time"
)
type ChatMessages struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
ChatSessionID uint `json:"chat_session_id" gorm:"type:int4;not null;index"`
SenderID uint `json:"sender_id" gorm:"type:int4;not null;index"`
Message string `json:"message" gorm:"type:text;not null"`
MessageType string `json:"message_type" gorm:"type:varchar(20);not null;default:'text';check:message_type IN ('text', 'image', 'file', 'user', 'assistant')"` // 'text', 'image', 'file', 'user', 'assistant'
IsEdited bool `json:"is_edited" gorm:"default:false"`
EditedAt *time.Time `json:"edited_at"`
IsDeleted bool `json:"is_deleted" gorm:"default:false"`
DeletedAt *time.Time `json:"deleted_at"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
// Relationships
ChatSession *ChatSessions `json:"chat_session" gorm:"foreignKey:ChatSessionID;references:ID"`
Sender *users.Users `json:"sender" gorm:"foreignKey:SenderID;references:ID"`
}

View File

@ -0,0 +1,21 @@
package entity
import (
"campaign-pool-be/app/database/entity/users"
"time"
)
type ChatParticipants struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
ChatSessionID uint `json:"chat_session_id" gorm:"type:int4;not null;index"`
UserID uint `json:"user_id" gorm:"type:int4;not null;index"`
JoinedAt time.Time `json:"joined_at" gorm:"default:now()"`
LeftAt *time.Time `json:"left_at"`
IsActive bool `json:"is_active" gorm:"default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
// Relationships
ChatSession *ChatSessions `json:"chat_session" gorm:"foreignKey:ChatSessionID;references:ID"`
User *users.Users `json:"user" gorm:"foreignKey:UserID;references:ID"`
}

View File

@ -0,0 +1,23 @@
package entity
import (
"time"
)
type ChatScheduleFiles struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
ChatScheduleID uint `json:"chat_schedule_id" gorm:"type:int4;not null;index"`
FileName string `json:"file_name" gorm:"type:varchar(255);not null"`
OriginalName string `json:"original_name" gorm:"type:varchar(255);not null"`
FilePath string `json:"file_path" gorm:"type:varchar(500);not null"`
FileSize int64 `json:"file_size" gorm:"type:int8"`
MimeType string `json:"mime_type" gorm:"type:varchar(100)"`
FileType string `json:"file_type" gorm:"type:varchar(20);not null;check:file_type IN ('article', 'journal', 'video', 'audio', 'document', 'other')"`
Description string `json:"description" gorm:"type:text"`
IsRequired bool `json:"is_required" gorm:"default:false"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
// Relationships
ChatSchedule *ChatSchedules `json:"chat_schedule" gorm:"foreignKey:ChatScheduleID;references:ID"`
}

View File

@ -0,0 +1,27 @@
package entity
import (
"campaign-pool-be/app/database/entity/users"
"time"
)
type ChatSchedules struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
ChatSessionID uint `json:"chat_session_id" gorm:"type:int4;not null;index"`
Title string `json:"title" gorm:"type:varchar(255);not null"`
Description string `json:"description" gorm:"type:text"`
Summary string `json:"summary" gorm:"type:text"`
ScheduledAt time.Time `json:"scheduled_at" gorm:"not null"`
Duration int `json:"duration" gorm:"type:int4;default:60"` // duration in minutes
Status string `json:"status" gorm:"type:varchar(20);not null;default:'scheduled';check:status IN ('scheduled', 'ongoing', 'completed', 'cancelled')"`
IsReminderSent bool `json:"is_reminder_sent" gorm:"default:false"`
ReminderSentAt *time.Time `json:"reminder_sent_at"`
CreatedBy uint `json:"created_by" gorm:"type:int4;not null;index"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
// Relationships
ChatSession *ChatSessions `json:"chat_session" gorm:"foreignKey:ChatSessionID;references:ID"`
Creator *users.Users `json:"creator" gorm:"foreignKey:CreatedBy;references:ID"`
Files []*ChatScheduleFiles `json:"files" gorm:"foreignKey:ChatScheduleID;references:ID"`
}

View File

@ -0,0 +1,20 @@
package entity
import (
"campaign-pool-be/app/database/entity/users"
"time"
)
type ChatSessions struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Name *string `json:"name" gorm:"type:varchar(255)"` // null for personal chat, filled for group chat
Type string `json:"type" gorm:"type:varchar(20);not null;default:'personal'"` // 'personal' or 'group'
CreatedBy uint `json:"created_by" gorm:"type:int4;not null;index"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
// Relationships
Creator *users.Users `json:"creator" gorm:"foreignKey:CreatedBy;references:ID"`
Participants []*ChatParticipants `json:"participants" gorm:"foreignKey:ChatSessionID;references:ID"`
Messages []*ChatMessages `json:"messages" gorm:"foreignKey:ChatSessionID;references:ID"`
}

View File

@ -0,0 +1,7 @@
package entity
type Cities struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
CityName string `json:"city_name" gorm:"type:varchar"`
ProvId int `json:"prov_id" gorm:"type:int4"`
}

View File

@ -0,0 +1,17 @@
package entity
import (
"campaign-pool-be/app/database/entity/users"
"time"
)
type Conversations struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Participant1ID uint `json:"participant1_id" gorm:"type:int4;not null;index"`
Participant2ID uint `json:"participant2_id" gorm:"type:int4;not null;index"`
LastMessageAt *time.Time `json:"last_message_at"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
Participant1 *users.Users `json:"participant1" gorm:"foreignKey:Participant1ID;references:ID"`
Participant2 *users.Users `json:"participant2" gorm:"foreignKey:Participant2ID;references:ID"`
}

View File

@ -0,0 +1,7 @@
package entity
type Districts struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
DisName string `json:"dis_name" gorm:"type:varchar"`
CityId int `json:"city_id" gorm:"type:int4"`
}

View File

@ -0,0 +1,26 @@
package entity
import (
users "campaign-pool-be/app/database/entity/users"
"time"
)
type EbookPurchases struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
BuyerId uint `json:"buyer_id" gorm:"type:int4"`
Buyer *users.Users `json:"buyer" gorm:"foreignKey:BuyerId;references:ID"`
EbookId uint `json:"ebook_id" gorm:"type:int4"`
Ebook *Ebooks `json:"ebook" gorm:"foreignKey:EbookId;references:ID"`
PurchasePrice float64 `json:"purchase_price" gorm:"type:decimal(10,2)"`
PaymentMethod *string `json:"payment_method" gorm:"type:varchar"`
PaymentStatus *string `json:"payment_status" gorm:"type:varchar;default:'pending'"`
TransactionId *string `json:"transaction_id" gorm:"type:varchar"`
PaymentProof *string `json:"payment_proof" gorm:"type:varchar"`
PaymentDate *time.Time `json:"payment_date" gorm:"type:timestamp"`
DownloadCount *int `json:"download_count" gorm:"type:int4;default:0"`
LastDownloadAt *time.Time `json:"last_download_at" gorm:"type:timestamp"`
StatusId *int `json:"status_id" gorm:"type:int4;default:1"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,24 @@
package entity
import (
users "campaign-pool-be/app/database/entity/users"
"time"
)
type EbookRatings struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
UserId uint `json:"user_id" gorm:"type:int4"`
User *users.Users `json:"user" gorm:"foreignKey:UserId;references:ID"`
EbookId uint `json:"ebook_id" gorm:"type:int4"`
Ebook *Ebooks `json:"ebook" gorm:"foreignKey:EbookId;references:ID"`
PurchaseId uint `json:"purchase_id" gorm:"type:int4"`
Purchase *EbookPurchases `json:"purchase" gorm:"foreignKey:PurchaseId;references:ID"`
Rating int `json:"rating" gorm:"type:int4;check:rating >= 1 AND rating <= 5"`
Review *string `json:"review" gorm:"type:text"`
IsAnonymous *bool `json:"is_anonymous" gorm:"type:bool;default:false"`
IsVerified *bool `json:"is_verified" gorm:"type:bool;default:true"`
StatusId *int `json:"status_id" gorm:"type:int4;default:1"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,16 @@
package entity
import (
users "campaign-pool-be/app/database/entity/users"
"time"
)
type EbookWishlists struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
UserId uint `json:"user_id" gorm:"type:int4"`
User *users.Users `json:"user" gorm:"foreignKey:UserId;references:ID"`
EbookId uint `json:"ebook_id" gorm:"type:int4"`
Ebook *Ebooks `json:"ebook" gorm:"foreignKey:EbookId;references:ID"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,40 @@
package entity
import (
users "campaign-pool-be/app/database/entity/users"
"time"
)
type Ebooks struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Title string `json:"title" gorm:"type:varchar"`
Slug string `json:"slug" gorm:"type:varchar"`
Description string `json:"description" gorm:"type:varchar"`
Price float64 `json:"price" gorm:"type:decimal(10,2)"`
PdfFilePath *string `json:"pdf_file_path" gorm:"type:varchar"`
PdfFileName *string `json:"pdf_file_name" gorm:"type:varchar"`
PdfFileSize *int64 `json:"pdf_file_size" gorm:"type:int8"`
ThumbnailPath *string `json:"thumbnail_path" gorm:"type:varchar"`
ThumbnailName *string `json:"thumbnail_name" gorm:"type:varchar"`
AuthorId uint `json:"author_id" gorm:"type:int4"`
Author *users.Users `json:"author" gorm:"foreignKey:AuthorId;references:ID"`
Category *string `json:"category" gorm:"type:varchar"`
Tags *string `json:"tags" gorm:"type:varchar"`
PageCount *int `json:"page_count" gorm:"type:int4"`
Language *string `json:"language" gorm:"type:varchar;default:'id'"`
Isbn *string `json:"isbn" gorm:"type:varchar"`
Publisher *string `json:"publisher" gorm:"type:varchar"`
PublishedYear *int `json:"published_year" gorm:"type:int4"`
DownloadCount *int `json:"download_count" gorm:"type:int4;default:0"`
PurchaseCount *int `json:"purchase_count" gorm:"type:int4;default:0"`
WishlistCount *int `json:"wishlist_count" gorm:"type:int4;default:0"`
Rating *float64 `json:"rating" gorm:"type:decimal(3,2);default:0"`
ReviewCount *int `json:"review_count" gorm:"type:int4;default:0"`
StatusId *int `json:"status_id" gorm:"type:int4;default:1"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
IsPublished *bool `json:"is_published" gorm:"type:bool;default:false"`
PublishedAt *time.Time `json:"published_at" gorm:"type:timestamp"`
CreatedById *uint `json:"created_by_id" gorm:"type:int4"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,19 @@
package entity
import (
"campaign-pool-be/app/database/entity/users"
"time"
)
type EducationHistory struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
UserID uint `json:"user_id" gorm:"type:int4;not null;index"`
SchoolName string `json:"school_name" gorm:"type:varchar;not null"`
Major string `json:"major" gorm:"type:varchar;not null"`
EducationLevel string `json:"education_level" gorm:"type:varchar;not null"`
GraduationYear int `json:"graduation_year" gorm:"type:int4;not null"`
CertificateImage *string `json:"certificate_image" gorm:"type:varchar"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
User *users.Users `json:"user" gorm:"foreignKey:UserID;references:ID"`
}

View File

@ -0,0 +1,20 @@
package entity
import (
"time"
)
type Feedbacks struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Message string `json:"message" gorm:"type:varchar"`
CommentFromName string `json:"comment_from_name" gorm:"type:varchar"`
CommentFromEmail string `json:"comment_from_email" gorm:"type:varchar"`
StatusId int `json:"status_id" gorm:"type:int4;default:0"`
ApprovedAt *time.Time `json:"approved_at" gorm:"type:timestamp"`
ReplyMessage *string `json:"reply_message" gorm:"type:varchar"`
IsActive bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}
// statusId => 0: waiting, 1: accepted, 2: replied, 3: nothing

View File

@ -0,0 +1,24 @@
package entity
import "time"
type MagazineFiles struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Title string `json:"title" gorm:"type:varchar"`
Description string `json:"description" gorm:"type:varchar"`
MagazineId uint `json:"magazine_id" gorm:"type:int4"`
DownloadCount *int `json:"download_count" gorm:"type:int4"`
StatusId int `json:"status_id" gorm:"type:int4"`
IsPublish *bool `json:"is_publish" gorm:"type:bool"`
FilePath *string `json:"file_path" gorm:"type:varchar"`
FileUrl *string `json:"file_url" gorm:"type:varchar"`
FileName *string `json:"file_name" gorm:"type:varchar"`
FileAlt *string `json:"file_alt" gorm:"type:varchar"`
WidthPixel *string `json:"width_pixel" gorm:"type:varchar"`
HeightPixel *string `json:"height_pixel" gorm:"type:varchar"`
Size *string `json:"size" gorm:"type:varchar"`
PublishedAt *time.Time `json:"published_at" gorm:"type:timestamp"`
IsActive bool `json:"is_active" gorm:"type:bool"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,20 @@
package entity
import "time"
type Magazines struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Title string `json:"title" gorm:"type:varchar"`
Description string `json:"description" gorm:"type:varchar"`
ThumbnailName *string `json:"thumbnail_name" gorm:"type:varchar"`
ThumbnailPath *string `json:"thumbnail_path" gorm:"type:varchar"`
ThumbnailUrl *string `json:"thumbnail_url" gorm:"type:varchar"`
PageUrl *string `json:"page_url" gorm:"type:varchar"`
CreatedById *uint `json:"created_by_id" gorm:"type:int4"`
StatusId int `json:"status_id" gorm:"type:int4"`
IsPublish *bool `json:"is_publish" gorm:"type:bool"`
PublishedAt *time.Time `json:"published_at" gorm:"type:timestamp"`
IsActive bool `json:"is_active" gorm:"type:bool"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,18 @@
package entity
import "time"
type MasterMenus struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Name string `json:"name" gorm:"type:varchar"`
Description string `json:"description" gorm:"type:varchar"`
ModuleId int `json:"module_id" gorm:"type:int4"`
ParentMenuId *int `json:"parent_menu_id" gorm:"type:int4"`
Icon *string `json:"icon" gorm:"type:varchar"`
Group string `json:"group" gorm:"type:varchar"`
Position *int `json:"position" gorm:"type:int4"`
StatusId int `json:"status_id" gorm:"type:int4"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,16 @@
package entity
import (
"time"
)
type MasterModules struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Name string `json:"name" gorm:"type:varchar"`
Description string `json:"description" gorm:"type:varchar"`
PathUrl string `json:"path_url" gorm:"type:varchar"`
StatusId int `json:"status_id" gorm:"type:int4"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,7 @@
package entity
type MasterStatuses struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Name string `json:"name" gorm:"type:varchar"`
IsActive bool `json:"is_active" gorm:"type:bool"`
}

View File

@ -0,0 +1,9 @@
package entity
type Provinces struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
ProvName string `json:"prov_name" gorm:"type:varchar"`
LocationId int `json:"location_id" gorm:"type:int4"`
Status int `json:"status" gorm:"type:int4"`
Timezone string `json:"timezone" gorm:"type:varchar"`
}

View File

@ -0,0 +1,18 @@
package entity
import (
"campaign-pool-be/app/database/entity/users"
"time"
)
type ResearchJournals struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
UserID uint `json:"user_id" gorm:"type:int4;not null;index"`
JournalTitle string `json:"journal_title" gorm:"type:varchar;not null"`
Publisher string `json:"publisher" gorm:"type:varchar;not null"`
JournalURL string `json:"journal_url" gorm:"type:varchar;not null"`
PublishedDate *time.Time `json:"published_date"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
User *users.Users `json:"user" gorm:"foreignKey:UserID;references:ID"`
}

View File

@ -0,0 +1,14 @@
package entity
import (
"time"
)
type Subscription struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Email string `json:"email" gorm:"type:varchar"`
IsActive bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,18 @@
package entity
import (
"campaign-pool-be/app/database/entity/users"
"time"
)
type WorkHistory struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
UserID uint `json:"user_id" gorm:"type:int4;not null;index"`
JobTitle string `json:"job_title" gorm:"type:varchar;not null"`
CompanyName string `json:"company_name" gorm:"type:varchar;not null"`
StartDate time.Time `json:"start_date" gorm:"not null"`
EndDate *time.Time `json:"end_date"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
User *users.Users `json:"user" gorm:"foreignKey:UserID;references:ID"`
}

View File

@ -2,6 +2,7 @@ package database
import (
"campaign-pool-be/app/database/entity"
"campaign-pool-be/app/database/entity/article_category_details"
"campaign-pool-be/app/database/entity/user_levels"
"campaign-pool-be/app/database/entity/users"
"campaign-pool-be/config/config"
@ -86,18 +87,55 @@ func Models() []interface{} {
return []interface{}{
entity.ActivityLogs{},
entity.ActivityLogTypes{},
entity.Advertisement{},
entity.Articles{},
entity.ArticleCategories{},
entity.ArticleApprovals{},
article_category_details.ArticleCategoryDetails{},
entity.ArticleFiles{},
entity.ArticleComments{},
entity.AuditTrails{},
entity.Cities{},
entity.CsrfTokenRecords{},
entity.CustomStaticPages{},
entity.Districts{},
entity.Feedbacks{},
entity.ForgotPasswords{},
entity.Magazines{},
entity.MagazineFiles{},
entity.MasterMenus{},
entity.MasterModules{},
entity.MasterStatuses{},
entity.MasterApprovalStatuses{},
entity.Provinces{},
entity.OneTimePasswords{},
entity.Subscription{},
user_levels.UserLevels{},
entity.UserRoles{},
entity.UserRoleAccesses{},
users.Users{},
entity.UserRoleLevelDetails{},
// Narasi Ahli entities
entity.EducationHistory{},
entity.WorkHistory{},
entity.ResearchJournals{},
entity.Conversations{},
entity.ChatMessages{},
entity.ChatParticipants{},
entity.ChatSessions{},
entity.ChatSchedules{},
entity.ChatScheduleFiles{},
entity.AIChatSessions{},
entity.AIChatMessages{},
entity.AIChatLogs{},
// Ebook entities
entity.Ebooks{},
entity.EbookWishlists{},
entity.EbookPurchases{},
entity.EbookRatings{},
// Campaign entities
entity.CampaignTypes{},
entity.CampaignDestinations{},

View File

@ -0,0 +1,46 @@
package seeds
import (
"campaign-pool-be/app/database/entity"
"gorm.io/gorm"
)
type MasterStatusesSeeder struct{}
var masterStatuses = []entity.MasterStatuses{
{
ID: 1,
Name: "Waiting",
IsActive: true,
},
{
ID: 2,
Name: "Active",
IsActive: true,
},
{
ID: 3,
Name: "Inactive",
IsActive: true,
},
}
func (MasterStatusesSeeder) Seed(conn *gorm.DB) error {
for _, row := range masterStatuses {
if err := conn.Create(&row).Error; err != nil {
return err
}
}
return nil
}
func (MasterStatusesSeeder) Count(conn *gorm.DB) (int, error) {
var count int64
if err := conn.Model(&entity.MasterStatuses{}).Count(&count).Error; err != nil {
return 0, err
}
return int(count), nil
}

View File

@ -6,6 +6,7 @@ import (
"campaign-pool-be/app/module/activity_logs/repository"
"campaign-pool-be/app/module/activity_logs/request"
"campaign-pool-be/app/module/activity_logs/response"
"campaign-pool-be/app/module/articles/service"
usersRepository "campaign-pool-be/app/module/users/repository"
"campaign-pool-be/utils/paginator"
utilSvc "campaign-pool-be/utils/service"
@ -17,6 +18,7 @@ import (
type activityLogsService struct {
Repo repository.ActivityLogsRepository
UsersRepo usersRepository.UsersRepository
ArticleService service.ArticlesService
Log zerolog.Logger
}
@ -31,12 +33,13 @@ type ActivityLogsService interface {
}
// 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{
Repo: repo,
Log: log,
UsersRepo: usersRepo,
ArticleService: articleService,
}
}
@ -78,6 +81,12 @@ func (_i *activityLogsService) Save(req request.ActivityLogsCreateRequest, authT
return nil, err
}
// update article
err = _i.ArticleService.UpdateActivityCount(*req.ArticleId, req.ActivityTypeId)
if err != nil {
return nil, err
}
return result, nil
}

View File

@ -0,0 +1,57 @@
package advertisement
import (
"campaign-pool-be/app/module/advertisement/controller"
"campaign-pool-be/app/module/advertisement/repository"
"campaign-pool-be/app/module/advertisement/service"
"github.com/gofiber/fiber/v2"
"go.uber.org/fx"
)
// struct of AdvertisementRouter
type AdvertisementRouter struct {
App fiber.Router
Controller *controller.Controller
}
// register bulky of Advertisement module
var NewAdvertisementModule = fx.Options(
// register repository of Advertisement module
fx.Provide(repository.NewAdvertisementRepository),
// register service of Advertisement module
fx.Provide(service.NewAdvertisementService),
// register controller of Advertisement module
fx.Provide(controller.NewController),
// register router of Advertisement module
fx.Provide(NewAdvertisementRouter),
)
// init AdvertisementRouter
func NewAdvertisementRouter(fiber *fiber.App, controller *controller.Controller) *AdvertisementRouter {
return &AdvertisementRouter{
App: fiber,
Controller: controller,
}
}
// register routes of Advertisement module
func (_i *AdvertisementRouter) RegisterAdvertisementRoutes() {
// define controllers
advertisementController := _i.Controller.Advertisement
// define routes
_i.App.Route("/advertisement", func(router fiber.Router) {
router.Get("/", advertisementController.All)
router.Get("/:id", advertisementController.Show)
router.Post("/", advertisementController.Save)
router.Post("/upload/:id", advertisementController.Upload)
router.Get("/viewer/:filename", advertisementController.Viewer)
router.Put("/:id", advertisementController.Update)
router.Put("/publish/:id", advertisementController.UpdatePublish)
router.Delete("/:id", advertisementController.Delete)
})
}

View File

@ -0,0 +1,284 @@
package controller
import (
"campaign-pool-be/app/module/advertisement/request"
"campaign-pool-be/app/module/advertisement/service"
"campaign-pool-be/utils/paginator"
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog"
utilRes "campaign-pool-be/utils/response"
utilVal "campaign-pool-be/utils/validator"
)
type advertisementController struct {
advertisementService service.AdvertisementService
Log zerolog.Logger
}
type AdvertisementController interface {
All(c *fiber.Ctx) error
Show(c *fiber.Ctx) error
Save(c *fiber.Ctx) error
Upload(c *fiber.Ctx) error
Update(c *fiber.Ctx) error
UpdatePublish(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error
Viewer(c *fiber.Ctx) error
}
func NewAdvertisementController(advertisementService service.AdvertisementService, log zerolog.Logger) AdvertisementController {
return &advertisementController{
advertisementService: advertisementService,
Log: log,
}
}
// All get all Advertisement
// @Summary Get all Advertisement
// @Description API for getting all Advertisement
// @Tags Advertisement
// @Security Bearer
// @Param req query request.AdvertisementQueryRequest false "query parameters"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /advertisement [get]
func (_i *advertisementController) All(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
reqContext := request.AdvertisementQueryRequestContext{
Title: c.Query("title"),
Description: c.Query("description"),
RedirectLink: c.Query("redirectLink"),
Placement: c.Query("placement"),
StatusId: c.Query("statusId"),
}
req := reqContext.ToParamRequest()
req.Pagination = paginate
advertisementData, paging, err := _i.advertisementService.All(req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Advertisement list successfully retrieved"},
Data: advertisementData,
Meta: paging,
})
}
// Show get one Advertisement
// @Summary Get one Advertisement
// @Description API for getting one Advertisement
// @Tags Advertisement
// @Security Bearer
// @Param id path int true "Advertisement ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /advertisement/{id} [get]
func (_i *advertisementController) Show(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
advertisementData, err := _i.advertisementService.Show(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Advertisement successfully retrieved"},
Data: advertisementData,
})
}
// Save create Advertisement
// @Summary Create Advertisement
// @Description API for create Advertisement
// @Tags Advertisement
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param payload body request.AdvertisementCreateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /advertisement [post]
func (_i *advertisementController) Save(c *fiber.Ctx) error {
req := new(request.AdvertisementCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
dataResult, err := _i.advertisementService.Save(*req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Advertisement successfully created"},
Data: dataResult,
})
}
// Upload Advertisement
// @Summary Upload Advertisement
// @Description API for Upload File Advertisement
// @Tags Advertisement
// @Security Bearer
// @Produce json
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param file formData file true "Upload file" multiple false
// @Param id path int true "Advertisement ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /advertisement/upload/{id} [post]
func (_i *advertisementController) Upload(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
err = _i.advertisementService.Upload(c, uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Advertisement successfully upload"},
})
}
// Update update Advertisement
// @Summary update Advertisement
// @Description API for update Advertisement
// @Tags Advertisement
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param payload body request.AdvertisementUpdateRequest true "Required payload"
// @Param id path int true "Advertisement ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /advertisement/{id} [put]
func (_i *advertisementController) Update(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
req := new(request.AdvertisementUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err = _i.advertisementService.Update(uint(id), *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Advertisement successfully updated"},
})
}
// UpdatePublish Advertisement
// @Summary Update Publish Advertisement
// @Description API for Update Publish Advertisement
// @Tags Advertisement
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param id path int true "Advertisement ID"
// @Param isPublish query bool true "Advertisement Publish Status"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /advertisement/publish/{id} [put]
func (_i *advertisementController) UpdatePublish(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
isPublish, err := strconv.ParseBool(c.Query("isPublish"))
if err != nil {
return err
}
err = _i.advertisementService.UpdatePublish(uint(id), isPublish)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Advertisement successfully publish updated"},
})
}
// Delete delete Advertisement
// @Summary delete Advertisement
// @Description API for delete Advertisement
// @Tags Advertisement
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param id path int true "Advertisement ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /advertisement/{id} [delete]
func (_i *advertisementController) Delete(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
err = _i.advertisementService.Delete(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Advertisement successfully deleted"},
})
}
// Viewer Advertisement
// @Summary Viewer Advertisement
// @Description API for Viewer Advertisement
// @Tags Advertisement
// @Security Bearer
// @Param filename path string true "Content File Name"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /advertisement/viewer/{filename} [get]
func (_i *advertisementController) Viewer(c *fiber.Ctx) error {
return _i.advertisementService.Viewer(c)
}

View File

@ -0,0 +1,17 @@
package controller
import (
"campaign-pool-be/app/module/advertisement/service"
"github.com/rs/zerolog"
)
type Controller struct {
Advertisement AdvertisementController
}
func NewController(AdvertisementService service.AdvertisementService, log zerolog.Logger) *Controller {
return &Controller{
Advertisement: NewAdvertisementController(AdvertisementService, log),
}
}

View File

@ -0,0 +1,28 @@
package mapper
import (
"campaign-pool-be/app/database/entity"
res "campaign-pool-be/app/module/advertisement/response"
)
func AdvertisementResponseMapper(advertisementReq *entity.Advertisement, host string) (advertisementRes *res.AdvertisementResponse) {
if advertisementReq != nil {
advertisementRes = &res.AdvertisementResponse{
ID: advertisementReq.ID,
Title: advertisementReq.Title,
Description: advertisementReq.Description,
RedirectLink: advertisementReq.RedirectLink,
Placement: advertisementReq.Placement,
StatusId: advertisementReq.StatusId,
IsActive: advertisementReq.IsActive,
IsPublish: advertisementReq.IsPublish,
CreatedAt: advertisementReq.CreatedAt,
UpdatedAt: advertisementReq.UpdatedAt,
}
if advertisementReq.ContentFilePath != nil {
advertisementRes.ContentFileUrl = host + "/advertisement/viewer/" + *advertisementReq.ContentFileName
}
}
return advertisementRes
}

View File

@ -0,0 +1,124 @@
package repository
import (
"campaign-pool-be/app/database"
"campaign-pool-be/app/database/entity"
"campaign-pool-be/app/module/advertisement/request"
"campaign-pool-be/utils/paginator"
utilSvc "campaign-pool-be/utils/service"
"fmt"
"strings"
"github.com/rs/zerolog"
)
type advertisementRepository struct {
DB *database.Database
Log zerolog.Logger
}
// AdvertisementRepository define interface of IAdvertisementRepository
type AdvertisementRepository interface {
GetAll(req request.AdvertisementQueryRequest) (advertisements []*entity.Advertisement, paging paginator.Pagination, err error)
FindOne(id uint) (advertisement *entity.Advertisement, err error)
FindByFilename(contentFilename string) (advertisement *entity.Advertisement, err error)
Create(advertisement *entity.Advertisement) (advertisementReturn *entity.Advertisement, err error)
Update(id uint, advertisement *entity.Advertisement) (err error)
Delete(id uint) (err error)
}
func NewAdvertisementRepository(db *database.Database, logger zerolog.Logger) AdvertisementRepository {
return &advertisementRepository{
DB: db,
Log: logger,
}
}
// implement interface of IAdvertisementRepository
func (_i *advertisementRepository) GetAll(req request.AdvertisementQueryRequest) (advertisements []*entity.Advertisement, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Model(&entity.Advertisement{})
query = query.Where("is_active = ?", true)
if req.Title != nil && *req.Title != "" {
title := strings.ToLower(*req.Title)
query = query.Where("LOWER(title) LIKE ?", "%"+strings.ToLower(title)+"%")
}
if req.Description != nil && *req.Description != "" {
description := strings.ToLower(*req.Description)
query = query.Where("LOWER(description) LIKE ?", "%"+strings.ToLower(description)+"%")
}
if req.RedirectLink != nil && *req.RedirectLink != "" {
redirectLink := strings.ToLower(*req.RedirectLink)
query = query.Where("LOWER(redirect_link) LIKE ?", "%"+strings.ToLower(redirectLink)+"%")
}
if req.Placement != nil && *req.Placement != "" {
placement := strings.ToLower(*req.Placement)
query = query.Where("LOWER(placement) LIKE ?", "%"+strings.ToLower(placement)+"%")
}
if req.StatusId != nil {
query = query.Where("status_id = ?", req.StatusId)
}
query.Count(&count)
if req.Pagination.SortBy != "" {
direction := "ASC"
if req.Pagination.Sort == "desc" {
direction = "DESC"
}
query.Order(fmt.Sprintf("%s %s", req.Pagination.SortBy, direction))
}
req.Pagination.Count = count
req.Pagination = paginator.Paging(req.Pagination)
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&advertisements).Error
if err != nil {
return
}
paging = *req.Pagination
return
}
func (_i *advertisementRepository) FindOne(id uint) (advertisement *entity.Advertisement, err error) {
query := _i.DB.DB.Where("id = ?", id)
if err := query.First(&advertisement).Error; err != nil {
return nil, err
}
return advertisement, nil
}
func (_i *advertisementRepository) FindByFilename(contentFilename string) (advertisement *entity.Advertisement, err error) {
query := _i.DB.DB.Where("content_file_name = ?", contentFilename)
if err := query.First(&advertisement).Error; err != nil {
return nil, err
}
return advertisement, nil
}
func (_i *advertisementRepository) Create(advertisement *entity.Advertisement) (advertisementReturn *entity.Advertisement, err error) {
result := _i.DB.DB.Create(advertisement)
return advertisement, result.Error
}
func (_i *advertisementRepository) Update(id uint, advertisement *entity.Advertisement) (err error) {
advertisementMap, err := utilSvc.StructToMap(advertisement)
if err != nil {
return err
}
query := _i.DB.DB.Model(&entity.Advertisement{}).Where(&entity.Advertisement{ID: id})
return query.Updates(advertisementMap).Error
}
func (_i *advertisementRepository) Delete(id uint) error {
query := _i.DB.DB.Model(&entity.Advertisement{}).Where("id = ?", id)
return query.Delete(&entity.Advertisement{}).Error
}

View File

@ -0,0 +1,100 @@
package request
import (
"campaign-pool-be/app/database/entity"
"campaign-pool-be/utils/paginator"
"strconv"
"time"
)
type AdvertisementGeneric interface {
ToEntity()
}
type AdvertisementQueryRequest struct {
Title *string `json:"title"`
Description *string `json:"description"`
RedirectLink *string `json:"redirectLink"`
Placement *string `json:"placement"`
IsPublish *bool `json:"isPublish"`
StatusId *int `json:"statusId"`
Pagination *paginator.Pagination `json:"pagination"`
}
type AdvertisementCreateRequest struct {
Title string `json:"title" validate:"required"`
Description string `json:"description" validate:"required"`
RedirectLink string `json:"redirectLink" validate:"required"`
Placement string `json:"placement" validate:"required"`
}
func (req AdvertisementCreateRequest) ToEntity() *entity.Advertisement {
return &entity.Advertisement{
Title: req.Title,
Description: req.Description,
RedirectLink: req.RedirectLink,
Placement: req.Placement,
StatusId: 1,
IsPublish: true,
IsActive: true,
}
}
type AdvertisementUpdateRequest struct {
ID uint `json:"id" validate:"required"`
Title string `json:"title" validate:"required"`
Description string `json:"description" validate:"required"`
RedirectLink string `json:"redirectLink" validate:"required"`
Placement string `json:"placement" validate:"required"`
}
func (req AdvertisementUpdateRequest) ToEntity() *entity.Advertisement {
return &entity.Advertisement{
ID: req.ID,
Title: req.Title,
Description: req.Description,
RedirectLink: req.RedirectLink,
Placement: req.Placement,
UpdatedAt: time.Now(),
}
}
type AdvertisementQueryRequestContext struct {
Title string `json:"title"`
Description string `json:"description"`
RedirectLink string `json:"redirectLink"`
Placement string `json:"placement"`
StatusId string `json:"statusId"`
IsPublish string `json:"isPublish"`
}
func (req AdvertisementQueryRequestContext) ToParamRequest() AdvertisementQueryRequest {
var request AdvertisementQueryRequest
if title := req.Title; title != "" {
request.Title = &title
}
if description := req.Description; description != "" {
request.Description = &description
}
if redirectLink := req.RedirectLink; redirectLink != "" {
request.RedirectLink = &redirectLink
}
if placement := req.Placement; placement != "" {
request.Placement = &placement
}
if isPublishStr := req.IsPublish; isPublishStr != "" {
isPublish, err := strconv.ParseBool(isPublishStr)
if err == nil {
request.IsPublish = &isPublish
}
}
if statusIdStr := req.StatusId; statusIdStr != "" {
statusId, err := strconv.Atoi(statusIdStr)
if err == nil {
request.StatusId = &statusId
}
}
return request
}

View File

@ -0,0 +1,17 @@
package response
import "time"
type AdvertisementResponse struct {
ID uint `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
RedirectLink string `json:"redirectLink"`
ContentFileUrl string `json:"contentFileUrl"`
Placement string `json:"placement"`
StatusId int `json:"statusId"`
IsPublish bool `json:"isPublish"`
IsActive bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}

View File

@ -0,0 +1,264 @@
package service
import (
"campaign-pool-be/app/database/entity"
"campaign-pool-be/app/module/advertisement/mapper"
"campaign-pool-be/app/module/advertisement/repository"
"campaign-pool-be/app/module/advertisement/request"
"campaign-pool-be/app/module/advertisement/response"
usersRepository "campaign-pool-be/app/module/users/repository"
config "campaign-pool-be/config/config"
minioStorage "campaign-pool-be/config/config"
"campaign-pool-be/utils/paginator"
"context"
"fmt"
"io"
"log"
"math/rand"
"mime"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/minio/minio-go/v7"
"github.com/rs/zerolog"
)
// AdvertisementService
type advertisementService struct {
Repo repository.AdvertisementRepository
UsersRepo usersRepository.UsersRepository
Log zerolog.Logger
Cfg *config.Config
MinioStorage *minioStorage.MinioStorage
}
// AdvertisementService define interface of IAdvertisementService
type AdvertisementService interface {
All(req request.AdvertisementQueryRequest) (advertisement []*response.AdvertisementResponse, paging paginator.Pagination, err error)
Show(id uint) (advertisement *response.AdvertisementResponse, err error)
Save(req request.AdvertisementCreateRequest) (advertisement *entity.Advertisement, err error)
Upload(c *fiber.Ctx, id uint) (err error)
Update(id uint, req request.AdvertisementUpdateRequest) (err error)
UpdatePublish(id uint, isPublish bool) (err error)
Delete(id uint) error
Viewer(c *fiber.Ctx) (err error)
}
// NewAdvertisementService init AdvertisementService
func NewAdvertisementService(repo repository.AdvertisementRepository, minioStorage *minioStorage.MinioStorage, usersRepo usersRepository.UsersRepository, log zerolog.Logger, cfg *config.Config) AdvertisementService {
return &advertisementService{
Repo: repo,
UsersRepo: usersRepo,
MinioStorage: minioStorage,
Log: log,
Cfg: cfg,
}
}
// All implement interface of AdvertisementService
func (_i *advertisementService) All(req request.AdvertisementQueryRequest) (advertisements []*response.AdvertisementResponse, paging paginator.Pagination, err error) {
results, paging, err := _i.Repo.GetAll(req)
if err != nil {
return
}
host := _i.Cfg.App.Domain
for _, result := range results {
advertisements = append(advertisements, mapper.AdvertisementResponseMapper(result, host))
}
return
}
func (_i *advertisementService) Show(id uint) (advertisement *response.AdvertisementResponse, err error) {
result, err := _i.Repo.FindOne(id)
if err != nil {
return nil, err
}
host := _i.Cfg.App.Domain
return mapper.AdvertisementResponseMapper(result, host), nil
}
func (_i *advertisementService) Save(req request.AdvertisementCreateRequest) (advertisement *entity.Advertisement, err error) {
_i.Log.Info().Interface("data", req).Msg("")
newReq := req.ToEntity()
return _i.Repo.Create(newReq)
}
func (_i *advertisementService) Upload(c *fiber.Ctx, id uint) (err error) {
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
form, err := c.MultipartForm()
if err != nil {
return err
}
//filess := form.File["files"]
// Create minio connection.
minioClient, err := _i.MinioStorage.ConnectMinio()
result, err := _i.Repo.FindOne(id)
if result == nil {
// Return status 400. Id not found.
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}
if err != nil {
// Return status 500 and minio connection error.
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}
for _, files := range form.File {
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Uploader:: top").
Interface("files", files).Msg("")
for _, fileHeader := range files {
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Uploader:: loop").
Interface("data", fileHeader).Msg("")
src, err := fileHeader.Open()
if err != nil {
return err
}
defer src.Close()
filename := filepath.Base(fileHeader.Filename)
filename = strings.ReplaceAll(filename, " ", "")
filenameWithoutExt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))])
extension := filepath.Ext(fileHeader.Filename)[1:]
now := time.Now()
rand.New(rand.NewSource(now.UnixNano()))
randUniqueId := rand.Intn(1000000)
newFilenameWithoutExt := filenameWithoutExt + "_" + strconv.Itoa(randUniqueId)
newFilename := newFilenameWithoutExt + "." + extension
objectName := fmt.Sprintf("advertisement/upload/%d/%d/%s", now.Year(), now.Month(), newFilename)
result.ContentFileName = &newFilename
result.ContentFilePath = &objectName
err = _i.Repo.Update(id, result)
if err != nil {
return err
}
// Upload file ke MinIO
_, err = minioClient.PutObject(context.Background(), bucketName, objectName, src, fileHeader.Size, minio.PutObjectOptions{})
if err != nil {
return err
}
}
}
return
}
func (_i *advertisementService) Update(id uint, req request.AdvertisementUpdateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("")
newReq := req.ToEntity()
return _i.Repo.Update(id, newReq)
}
func (_i *advertisementService) UpdatePublish(id uint, isPublish bool) (err error) {
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "UpdatePublish").
Interface("ids", id).Msg("")
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "UpdatePublish").
Interface("isPublish", isPublish).Msg("")
result, err := _i.Repo.FindOne(id)
if err != nil {
return err
}
result.IsPublish = isPublish
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "UpdatePublish").
Interface("result", result).Msg("")
return _i.Repo.Update(id, result)
}
func (_i *advertisementService) Delete(id uint) error {
result, err := _i.Repo.FindOne(id)
if err != nil {
return err
}
result.IsActive = false
return _i.Repo.Update(id, result)
}
func (_i *advertisementService) Viewer(c *fiber.Ctx) (err error) {
filename := c.Params("filename")
result, err := _i.Repo.FindByFilename(filename)
if err != nil {
return err
}
ctx := context.Background()
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
objectName := *result.ContentFilePath
// Create minio connection.
minioClient, err := _i.MinioStorage.ConnectMinio()
if err != nil {
// Return status 500 and minio connection error.
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}
fileContent, err := minioClient.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{})
if err != nil {
log.Fatalln(err)
}
defer fileContent.Close()
// Tentukan Content-Type berdasarkan ekstensi file
contentType := mime.TypeByExtension("." + getFileExtension(objectName))
if contentType == "" {
contentType = "application/octet-stream" // fallback jika tidak ada tipe MIME yang cocok
}
c.Set("Content-Type", contentType)
if _, err := io.Copy(c.Response().BodyWriter(), fileContent); err != nil {
return err
}
return
}
func getFileExtension(filename string) string {
// split file name
parts := strings.Split(filename, ".")
// jika tidak ada ekstensi, kembalikan string kosong
if len(parts) == 1 || (len(parts) == 2 && parts[0] == "") {
return ""
}
// ambil ekstensi terakhir
return parts[len(parts)-1]
}

View File

@ -0,0 +1,65 @@
package ai_chat
import (
"campaign-pool-be/app/module/ai_chat/controller"
"campaign-pool-be/app/module/ai_chat/repository"
"campaign-pool-be/app/module/ai_chat/service"
"github.com/gofiber/fiber/v2"
"go.uber.org/fx"
)
// struct of AIChatRouter
type AIChatRouter struct {
App fiber.Router
Controller controller.AIChatController
}
// register bulky of AI Chat module
var NewAIChatModule = fx.Options(
// register repository of AI Chat module
fx.Provide(repository.NewAIChatRepository),
// register service of AI Chat module
fx.Provide(service.NewAIChatService),
// register controller of AI Chat module
fx.Provide(controller.NewAIChatController),
// register router of AI Chat module
fx.Provide(NewAIChatRouter),
)
// init AIChatRouter
func NewAIChatRouter(fiber *fiber.App, controller controller.AIChatController) *AIChatRouter {
return &AIChatRouter{
App: fiber,
Controller: controller,
}
}
// register routes of AI Chat module
func (_i *AIChatRouter) RegisterAIChatRoutes() {
// define controllers
aiChatController := _i.Controller
// define routes
_i.App.Route("/ai-chat", func(router fiber.Router) {
// Sessions routes
router.Get("/sessions", aiChatController.GetUserSessions)
router.Get("/sessions/:id", aiChatController.GetSession)
router.Post("/sessions", aiChatController.CreateSession)
router.Put("/sessions/:id", aiChatController.UpdateSession)
router.Delete("/sessions/:id", aiChatController.DeleteSession)
// Messages routes
router.Get("/sessions/:id/messages", aiChatController.GetSessionMessages)
router.Post("/sessions/messages", aiChatController.SendMessage)
router.Put("/sessions/messages/:messageId", aiChatController.UpdateMessage)
router.Delete("/sessions/messages/:messageId", aiChatController.DeleteMessage)
// Logs routes
router.Get("/logs", aiChatController.GetUserLogs)
router.Get("/logs/:id", aiChatController.GetLog)
})
}

View File

@ -0,0 +1,438 @@
package controller
import (
"campaign-pool-be/app/module/ai_chat/request"
"campaign-pool-be/app/module/ai_chat/service"
"campaign-pool-be/utils/paginator"
utilRes "campaign-pool-be/utils/response"
utilVal "campaign-pool-be/utils/validator"
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog"
)
type aiChatController struct {
aiChatService service.AIChatService
Log zerolog.Logger
}
type AIChatController interface {
// AI Chat Sessions
GetUserSessions(c *fiber.Ctx) error
GetSession(c *fiber.Ctx) error
CreateSession(c *fiber.Ctx) error
UpdateSession(c *fiber.Ctx) error
DeleteSession(c *fiber.Ctx) error
// AI Chat Messages
GetSessionMessages(c *fiber.Ctx) error
SendMessage(c *fiber.Ctx) error
UpdateMessage(c *fiber.Ctx) error
DeleteMessage(c *fiber.Ctx) error
// AI Chat Logs
GetUserLogs(c *fiber.Ctx) error
GetLog(c *fiber.Ctx) error
}
func NewAIChatController(aiChatService service.AIChatService, log zerolog.Logger) AIChatController {
return &aiChatController{
aiChatService: aiChatService,
Log: log,
}
}
// Get User Sessions
// @Summary Get user AI chat sessions
// @Description API for getting all AI chat sessions for authenticated user
// @Tags AI Chat
// @Security Bearer
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param req query request.AIChatSessionsQueryRequest false "query parameters"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ai-chat/sessions [get]
func (_i *aiChatController) GetUserSessions(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
authHeader := c.Get("Authorization")
reqContext := request.AIChatSessionsQueryRequestContext{
IsActive: c.Query("isActive"),
}
req := reqContext.ToParamRequest()
req.Pagination = paginate
sessionsData, paging, err := _i.aiChatService.GetUserSessions(authHeader, req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"AI chat sessions successfully retrieved"},
Data: sessionsData,
Meta: paging,
})
}
// Get Session
// @Summary Get one AI chat session
// @Description API for getting one AI chat session
// @Tags AI Chat
// @Security Bearer
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param id path int true "Session ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ai-chat/sessions/{id} [get]
func (_i *aiChatController) GetSession(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
authHeader := c.Get("Authorization")
sessionData, err := _i.aiChatService.GetSession(authHeader, uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"AI chat session successfully retrieved"},
Data: sessionData,
})
}
// Create Session
// @Summary Create AI chat session
// @Description API for create AI chat session
// @Tags AI Chat
// @Security Bearer
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param payload body request.AIChatSessionsCreateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ai-chat/sessions [post]
func (_i *aiChatController) CreateSession(c *fiber.Ctx) error {
req := new(request.AIChatSessionsCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
authHeader := c.Get("Authorization")
dataResult, err := _i.aiChatService.CreateSession(authHeader, *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"AI chat session successfully created"},
Data: dataResult,
})
}
// Update Session
// @Summary Update AI chat session
// @Description API for update AI chat session
// @Tags AI Chat
// @Security Bearer
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param id path int true "Session ID"
// @Param payload body request.AIChatSessionsUpdateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ai-chat/sessions/{id} [put]
func (_i *aiChatController) UpdateSession(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
req := new(request.AIChatSessionsUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
authHeader := c.Get("Authorization")
err = _i.aiChatService.UpdateSession(authHeader, uint(id), *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"AI chat session successfully updated"},
})
}
// Delete Session
// @Summary Delete AI chat session
// @Description API for delete AI chat session
// @Tags AI Chat
// @Security Bearer
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param id path int true "Session ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ai-chat/sessions/{id} [delete]
func (_i *aiChatController) DeleteSession(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
authHeader := c.Get("Authorization")
err = _i.aiChatService.DeleteSession(authHeader, uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"AI chat session successfully deleted"},
})
}
// Get Session Messages
// @Summary Get AI chat session messages
// @Description API for getting all messages in an AI chat session
// @Tags AI Chat
// @Security Bearer
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param id path int true "Session ID"
// @Param req query request.AIChatMessagesQueryRequest false "query parameters"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ai-chat/sessions/{id}/messages [get]
func (_i *aiChatController) GetSessionMessages(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
authHeader := c.Get("Authorization")
reqContext := request.AIChatMessagesQueryRequestContext{}
req := reqContext.ToParamRequest()
req.Pagination = paginate
messagesData, paging, err := _i.aiChatService.GetSessionMessages(authHeader, uint(id), req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"AI chat messages successfully retrieved"},
Data: messagesData,
Meta: paging,
})
}
// Send Message
// @Summary Send message to AI chat session
// @Description API for sending a message to an AI chat session
// @Tags AI Chat
// @Security Bearer
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param payload body request.AIChatMessagesCreateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ai-chat/sessions/messages [post]
func (_i *aiChatController) SendMessage(c *fiber.Ctx) error {
req := new(request.AIChatMessagesCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
authHeader := c.Get("Authorization")
dataResult, err := _i.aiChatService.SendMessage(authHeader, *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Message successfully sent"},
Data: dataResult,
})
}
// Update Message
// @Summary Update AI chat message
// @Description API for update AI chat message
// @Tags AI Chat
// @Security Bearer
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param messageId path int true "Message ID"
// @Param payload body request.AIChatMessagesUpdateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ai-chat/sessions/messages/{messageId} [put]
func (_i *aiChatController) UpdateMessage(c *fiber.Ctx) error {
messageId, err := strconv.ParseUint(c.Params("messageId"), 10, 0)
if err != nil {
return err
}
req := new(request.AIChatMessagesUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
authHeader := c.Get("Authorization")
err = _i.aiChatService.UpdateMessage(authHeader, uint(messageId), *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"AI chat message successfully updated"},
})
}
// Delete Message
// @Summary Delete AI chat message
// @Description API for delete AI chat message
// @Tags AI Chat
// @Security Bearer
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param messageId path int true "Message ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ai-chat/sessions/messages/{messageId} [delete]
func (_i *aiChatController) DeleteMessage(c *fiber.Ctx) error {
messageId, err := strconv.ParseUint(c.Params("messageId"), 10, 0)
if err != nil {
return err
}
authHeader := c.Get("Authorization")
err = _i.aiChatService.DeleteMessage(authHeader, uint(messageId))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"AI chat message successfully deleted"},
})
}
// Get User Logs
// @Summary Get user AI chat logs
// @Description API for getting all AI chat logs for authenticated user
// @Tags AI Chat
// @Security Bearer
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param req query request.AIChatLogsQueryRequest false "query parameters"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ai-chat/logs [get]
func (_i *aiChatController) GetUserLogs(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
authHeader := c.Get("Authorization")
reqContext := request.AIChatLogsQueryRequestContext{
LogType: c.Query("logType"),
}
req := reqContext.ToParamRequest()
req.Pagination = paginate
logsData, paging, err := _i.aiChatService.GetUserLogs(authHeader, req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"AI chat logs successfully retrieved"},
Data: logsData,
Meta: paging,
})
}
// Get Log
// @Summary Get one AI chat log
// @Description API for getting one AI chat log
// @Tags AI Chat
// @Security Bearer
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param id path int true "Log ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ai-chat/logs/{id} [get]
func (_i *aiChatController) GetLog(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
authHeader := c.Get("Authorization")
logData, err := _i.aiChatService.GetLog(authHeader, uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"AI chat log successfully retrieved"},
Data: logData,
})
}

View File

@ -0,0 +1,71 @@
package mapper
import (
"campaign-pool-be/app/database/entity"
"campaign-pool-be/app/module/ai_chat/response"
)
func AIChatSessionsResponseMapper(session *entity.AIChatSessions) *response.AIChatSessionsResponse {
result := &response.AIChatSessionsResponse{
ID: session.ID,
SessionID: session.SessionID,
UserID: session.UserID,
AgentID: session.AgentID,
Title: session.Title,
MessageCount: session.MessageCount,
IsActive: session.IsActive,
CreatedAt: session.CreatedAt,
UpdatedAt: session.UpdatedAt,
}
if session.User != nil {
result.User = &response.UserBasicInfo{
ID: session.User.ID,
Username: session.User.Username,
Fullname: session.User.Fullname,
Email: session.User.Email,
}
}
return result
}
func AIChatMessagesResponseMapper(message *entity.AIChatMessages) *response.AIChatMessagesResponse {
return &response.AIChatMessagesResponse{
ID: message.ID,
SessionID: message.SessionID,
MessageType: message.MessageType,
Content: message.Content,
IsActive: message.IsActive,
CreatedAt: message.CreatedAt,
}
}
func AIChatSessionWithMessagesResponseMapper(session *entity.AIChatSessions, messages []*entity.AIChatMessages) *response.AIChatSessionWithMessagesResponse {
sessionResponse := AIChatSessionsResponseMapper(session)
var messagesResponse []*response.AIChatMessagesResponse
for _, message := range messages {
messagesResponse = append(messagesResponse, AIChatMessagesResponseMapper(message))
}
return &response.AIChatSessionWithMessagesResponse{
Session: sessionResponse,
Messages: messagesResponse,
}
}
func AIChatLogsResponseMapper(log *entity.AIChatLogs) *response.AIChatLogsResponse {
result := &response.AIChatLogsResponse{
ID: log.ID,
UserID: log.UserID,
SessionID: log.SessionID,
StartDate: log.StartDate,
EndDate: log.EndDate,
TotalDuration: log.TotalDuration,
CreatedAt: log.CreatedAt,
UpdatedAt: log.UpdatedAt,
}
return result
}

View File

@ -0,0 +1,198 @@
package repository
import (
"campaign-pool-be/app/database"
"campaign-pool-be/app/database/entity"
"campaign-pool-be/app/module/ai_chat/request"
"campaign-pool-be/utils/paginator"
)
type aiChatRepository struct {
DB *database.Database
}
type AIChatRepository interface {
// AI Chat Sessions
GetUserSessions(userId uint, req request.AIChatSessionsQueryRequest) (sessions []*entity.AIChatSessions, paging paginator.Pagination, err error)
FindSessionByUserAndId(userId uint, sessionId uint) (session *entity.AIChatSessions, err error)
FindSessionBySessionId(sessionId string) (session *entity.AIChatSessions, err error)
CreateSession(session *entity.AIChatSessions) (result *entity.AIChatSessions, err error)
UpdateSession(userId uint, sessionId uint, session *entity.AIChatSessions) (err error)
DeleteSession(userId uint, sessionId uint) (err error)
IncrementMessageCount(sessionId uint) (err error)
// AI Chat Messages
GetSessionMessages(sessionId string, req request.AIChatMessagesQueryRequest) (messages []*entity.AIChatMessages, paging paginator.Pagination, err error)
CreateMessage(message *entity.AIChatMessages) (result *entity.AIChatMessages, err error)
UpdateMessage(messageId uint, message *entity.AIChatMessages) (err error)
DeleteMessage(messageId uint) (err error)
GetLastMessage(sessionId string) (message *entity.AIChatMessages, err error)
// AI Chat Logs
GetUserLogs(userId uint, req request.AIChatLogsQueryRequest) (logs []*entity.AIChatLogs, paging paginator.Pagination, err error)
FindLogByUserAndId(userId uint, logId uint) (log *entity.AIChatLogs, err error)
}
func NewAIChatRepository(db *database.Database) AIChatRepository {
return &aiChatRepository{
DB: db,
}
}
// AI Chat Sessions methods
func (_i *aiChatRepository) GetUserSessions(userId uint, req request.AIChatSessionsQueryRequest) (sessions []*entity.AIChatSessions, paging paginator.Pagination, err error) {
query := _i.DB.DB.Model(&entity.AIChatSessions{}).Where("user_id = ?", userId)
// Apply filters
if req.IsActive != nil {
query = query.Where("is_active = ?", *req.IsActive)
}
// Include user relationship
// query = query.Preload("User")
// Order by updated_at desc (most recent first)
query = query.Order("updated_at DESC")
// Apply pagination
var count int64
query.Count(&count)
req.Pagination.Count = count
req.Pagination = paginator.Paging(req.Pagination)
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&sessions).Error
paging = *req.Pagination
return
}
func (_i *aiChatRepository) FindSessionByUserAndId(userId uint, sessionId uint) (session *entity.AIChatSessions, err error) {
err = _i.DB.DB.Model(&entity.AIChatSessions{}).Where("user_id = ? AND id = ?", userId, sessionId).Preload("User").First(&session).Error
return
}
func (_i *aiChatRepository) FindSessionBySessionId(sessionId string) (session *entity.AIChatSessions, err error) {
err = _i.DB.DB.Model(&entity.AIChatSessions{}).Where("session_id = ?", sessionId).Preload("User").First(&session).Error
return
}
func (_i *aiChatRepository) CreateSession(session *entity.AIChatSessions) (result *entity.AIChatSessions, err error) {
err = _i.DB.DB.Create(session).Error
if err != nil {
return nil, err
}
// Reload with relationships
err = _i.DB.DB.Model(&entity.AIChatSessions{}).Preload("User").First(&result, session.ID).Error
return
}
func (_i *aiChatRepository) UpdateSession(userId uint, sessionId uint, session *entity.AIChatSessions) (err error) {
err = _i.DB.DB.Model(&entity.AIChatSessions{}).Where("user_id = ? AND id = ?", userId, sessionId).Updates(session).Error
return
}
func (_i *aiChatRepository) DeleteSession(userId uint, sessionId uint) (err error) {
err = _i.DB.DB.Model(&entity.AIChatSessions{}).Where("user_id = ? AND id = ?", userId, sessionId).Delete(&entity.AIChatSessions{}).Error
return
}
func (_i *aiChatRepository) IncrementMessageCount(sessionId uint) (err error) {
err = _i.DB.DB.Exec("UPDATE ai_chat_sessions SET message_count = message_count + 1 WHERE id = ?", sessionId).Error
return
}
// AI Chat Messages methods
func (_i *aiChatRepository) GetSessionMessages(sessionId string, req request.AIChatMessagesQueryRequest) (messages []*entity.AIChatMessages, paging paginator.Pagination, err error) {
query := _i.DB.DB.Model(&entity.AIChatMessages{}).Where("session_id = ?", sessionId)
// Order by created_at asc (oldest first for chat)
query = query.Order("created_at ASC")
// Apply pagination
var count int64
query.Count(&count)
req.Pagination.Count = count
req.Pagination = paginator.Paging(req.Pagination)
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&messages).Error
paging = *req.Pagination
return
}
func (_i *aiChatRepository) CreateMessage(message *entity.AIChatMessages) (result *entity.AIChatMessages, err error) {
err = _i.DB.DB.Create(message).Error
if err != nil {
return nil, err
}
// Reload
err = _i.DB.DB.Model(&entity.AIChatMessages{}).First(&result, message.ID).Error
return
}
func (_i *aiChatRepository) UpdateMessage(messageId uint, message *entity.AIChatMessages) (err error) {
err = _i.DB.DB.Model(&entity.AIChatMessages{}).Where("id = ?", messageId).Updates(message).Error
return
}
func (_i *aiChatRepository) DeleteMessage(messageId uint) (err error) {
err = _i.DB.DB.Model(&entity.AIChatMessages{}).Where("id = ?", messageId).Delete(&entity.AIChatMessages{}).Error
return
}
// AI Chat Logs methods
func (_i *aiChatRepository) GetUserLogs(userId uint, req request.AIChatLogsQueryRequest) (logs []*entity.AIChatLogs, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Model(&entity.AIChatLogs{}).Where("user_id = ?", userId)
// Note: AIChatLogs entity doesn't have LogType field, so we skip this filter
// if req.LogType != nil && *req.LogType != "" {
// query = query.Where("log_type = ?", *req.LogType)
// }
// Count total records
err = query.Count(&count).Error
if err != nil {
return
}
// Apply pagination
if req.Pagination != nil {
query = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit)
}
// Order by created_at desc (newest first)
query = query.Order("created_at DESC")
err = query.Find(&logs).Error
if err != nil {
return
}
// Set pagination info
if req.Pagination != nil {
paging = *req.Pagination
paging.Count = count
paging.TotalPage = int((count + int64(req.Pagination.Limit) - 1) / int64(req.Pagination.Limit))
}
return
}
func (_i *aiChatRepository) FindLogByUserAndId(userId uint, logId uint) (log *entity.AIChatLogs, err error) {
query := _i.DB.DB.Model(&entity.AIChatLogs{}).Where("user_id = ? AND id = ?", userId, logId)
if err := query.First(&log).Error; err != nil {
return nil, err
}
return
}
func (_i *aiChatRepository) GetLastMessage(sessionId string) (message *entity.AIChatMessages, err error) {
err = _i.DB.DB.Model(&entity.AIChatMessages{}).Where("session_id = ?", sessionId).Order("created_at DESC").First(&message).Error
return
}

View File

@ -0,0 +1,114 @@
package request
import (
"campaign-pool-be/app/database/entity"
"campaign-pool-be/utils/paginator"
)
// AI Chat Sessions Request DTOs
type AIChatSessionsQueryRequest struct {
IsActive *bool `json:"isActive"`
Pagination *paginator.Pagination `json:"pagination"`
}
type AIChatSessionsCreateRequest struct {
SessionID string `json:"sessionId" validate:"required"`
Title string `json:"title" validate:"required,min=2,max=255"`
AgentID string `json:"agentId" validate:"required"`
}
func (req AIChatSessionsCreateRequest) ToEntity() *entity.AIChatSessions {
return &entity.AIChatSessions{
SessionID: req.SessionID,
AgentID: req.AgentID,
Title: req.Title,
MessageCount: 0,
IsActive: true,
}
}
type AIChatSessionsUpdateRequest struct {
Title string `json:"title" validate:"required,min=2,max=255"`
IsActive bool `json:"isActive"`
}
func (req AIChatSessionsUpdateRequest) ToEntity() *entity.AIChatSessions {
return &entity.AIChatSessions{
Title: req.Title,
IsActive: req.IsActive,
}
}
// AI Chat Messages Request DTOs
type AIChatMessagesQueryRequest struct {
Pagination *paginator.Pagination `json:"pagination"`
}
type AIChatMessagesCreateRequest struct {
SessionID string `json:"sessionId" validate:"required"`
MessageType string `json:"messageType" validate:"required,oneof=user assistant"`
Content string `json:"content" validate:"required,min=1"`
}
func (req AIChatMessagesCreateRequest) ToEntity() *entity.AIChatMessages {
return &entity.AIChatMessages{
SessionID: req.SessionID,
MessageType: req.MessageType,
Content: req.Content,
IsActive: true,
}
}
type AIChatMessagesUpdateRequest struct {
Content string `json:"content" validate:"required,min=1"`
}
func (req AIChatMessagesUpdateRequest) ToEntity() *entity.AIChatMessages {
return &entity.AIChatMessages{
Content: req.Content,
}
}
type AIChatSessionsQueryRequestContext struct {
IsActive string `json:"isActive"`
}
func (req AIChatSessionsQueryRequestContext) ToParamRequest() AIChatSessionsQueryRequest {
var request AIChatSessionsQueryRequest
if isActiveStr := req.IsActive; isActiveStr != "" {
isActive := isActiveStr == "true"
request.IsActive = &isActive
}
return request
}
type AIChatMessagesQueryRequestContext struct {
}
func (req AIChatMessagesQueryRequestContext) ToParamRequest() AIChatMessagesQueryRequest {
var request AIChatMessagesQueryRequest
return request
}
// AI Chat Logs Request DTOs
type AIChatLogsQueryRequest struct {
LogType *string `json:"logType"`
Pagination *paginator.Pagination `json:"pagination"`
}
type AIChatLogsQueryRequestContext struct {
LogType string `json:"logType"`
}
func (req AIChatLogsQueryRequestContext) ToParamRequest() AIChatLogsQueryRequest {
var request AIChatLogsQueryRequest
if logType := req.LogType; logType != "" {
request.LogType = &logType
}
return request
}

View File

@ -0,0 +1,63 @@
package response
import (
"time"
)
// AI Chat Sessions Response DTOs
type AIChatSessionsResponse struct {
ID uint `json:"id"`
SessionID string `json:"sessionId"`
UserID uint `json:"userId"`
AgentID string `json:"agentId"`
Title string `json:"title"`
MessageCount int `json:"messageCount"`
IsActive bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
// Nested user info
User *UserBasicInfo `json:"user,omitempty"`
// Last message preview
LastMessage *AIChatMessagesResponse `json:"lastMessage,omitempty"`
}
// AI Chat Messages Response DTOs
type AIChatMessagesResponse struct {
ID uint `json:"id"`
SessionID string `json:"sessionId"`
MessageType string `json:"messageType"`
Content string `json:"content"`
IsActive bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
}
type UserBasicInfo struct {
ID uint `json:"id"`
Username string `json:"username"`
Fullname string `json:"fullname"`
Email string `json:"email"`
}
// Session with messages
type AIChatSessionWithMessagesResponse struct {
Session *AIChatSessionsResponse `json:"session"`
Messages []*AIChatMessagesResponse `json:"messages"`
Pagination interface{} `json:"pagination"`
}
// AI Chat Logs Response DTOs
type AIChatLogsResponse struct {
ID uint `json:"id"`
UserID uint `json:"userId"`
SessionID uint `json:"sessionId"`
StartDate time.Time `json:"startDate"`
EndDate *time.Time `json:"endDate"`
TotalDuration int64 `json:"totalDuration"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
// Nested user info
User *UserBasicInfo `json:"user,omitempty"`
}

View File

@ -0,0 +1,229 @@
package service
import (
"campaign-pool-be/app/module/ai_chat/mapper"
"campaign-pool-be/app/module/ai_chat/repository"
"campaign-pool-be/app/module/ai_chat/request"
"campaign-pool-be/app/module/ai_chat/response"
usersRepository "campaign-pool-be/app/module/users/repository"
"campaign-pool-be/utils/paginator"
utilSvc "campaign-pool-be/utils/service"
"errors"
"github.com/rs/zerolog"
)
type aiChatService struct {
Repo repository.AIChatRepository
UsersRepo usersRepository.UsersRepository
Log zerolog.Logger
}
type AIChatService interface {
// Sessions
GetUserSessions(authToken string, req request.AIChatSessionsQueryRequest) (sessions []*response.AIChatSessionsResponse, paging paginator.Pagination, err error)
GetSession(authToken string, id uint) (session *response.AIChatSessionsResponse, err error)
CreateSession(authToken string, req request.AIChatSessionsCreateRequest) (session *response.AIChatSessionsResponse, err error)
UpdateSession(authToken string, id uint, req request.AIChatSessionsUpdateRequest) (err error)
DeleteSession(authToken string, id uint) error
ArchiveSession(authToken string, id uint) error
// Messages
GetSessionMessages(authToken string, sessionId uint, req request.AIChatMessagesQueryRequest) (messages []*response.AIChatMessagesResponse, paging paginator.Pagination, err error)
SendMessage(authToken string, req request.AIChatMessagesCreateRequest) (message *response.AIChatMessagesResponse, err error)
UpdateMessage(authToken string, messageId uint, req request.AIChatMessagesUpdateRequest) (err error)
DeleteMessage(authToken string, messageId uint) error
// Logs
GetUserLogs(authToken string, req request.AIChatLogsQueryRequest) (logs []*response.AIChatLogsResponse, paging paginator.Pagination, err error)
GetLog(authToken string, logId uint) (log *response.AIChatLogsResponse, err error)
}
func NewAIChatService(repo repository.AIChatRepository, usersRepo usersRepository.UsersRepository, log zerolog.Logger) AIChatService {
return &aiChatService{
Repo: repo,
UsersRepo: usersRepo,
Log: log,
}
}
// Sessions methods
func (_i *aiChatService) GetUserSessions(authToken string, req request.AIChatSessionsQueryRequest) (sessions []*response.AIChatSessionsResponse, paging paginator.Pagination, err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
results, paging, err := _i.Repo.GetUserSessions(userInfo.ID, req)
if err != nil {
return
}
for _, result := range results {
sessions = append(sessions, mapper.AIChatSessionsResponseMapper(result))
}
return
}
func (_i *aiChatService) GetSession(authToken string, id uint) (session *response.AIChatSessionsResponse, err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
result, err := _i.Repo.FindSessionByUserAndId(userInfo.ID, id)
if err != nil {
return nil, err
}
return mapper.AIChatSessionsResponseMapper(result), nil
}
func (_i *aiChatService) CreateSession(authToken string, req request.AIChatSessionsCreateRequest) (session *response.AIChatSessionsResponse, err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
entity := req.ToEntity()
entity.UserID = userInfo.ID
_i.Log.Info().Interface("data", entity).Msg("Create AI chat session")
result, err := _i.Repo.CreateSession(entity)
if err != nil {
return nil, err
}
return mapper.AIChatSessionsResponseMapper(result), nil
}
func (_i *aiChatService) UpdateSession(authToken string, id uint, req request.AIChatSessionsUpdateRequest) (err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
_i.Log.Info().Interface("data", req).Msg("Updating AI chat session")
// Check if session exists and belongs to user
existing, err := _i.Repo.FindSessionByUserAndId(userInfo.ID, id)
if err != nil {
return err
}
if existing == nil {
return errors.New("AI chat session not found")
}
entity := req.ToEntity()
return _i.Repo.UpdateSession(userInfo.ID, id, entity)
}
func (_i *aiChatService) DeleteSession(authToken string, id uint) error {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
// Check if session exists and belongs to user
existing, err := _i.Repo.FindSessionByUserAndId(userInfo.ID, id)
if err != nil {
return err
}
if existing == nil {
return errors.New("AI chat session not found")
}
return _i.Repo.DeleteSession(userInfo.ID, id)
}
func (_i *aiChatService) ArchiveSession(authToken string, id uint) error {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
// Check if session exists and belongs to user
existing, err := _i.Repo.FindSessionByUserAndId(userInfo.ID, id)
if err != nil {
return err
}
if existing == nil {
return errors.New("AI chat session not found")
}
// Update status to archived
existing.IsActive = false
return _i.Repo.UpdateSession(userInfo.ID, id, existing)
}
// Messages methods
func (_i *aiChatService) GetSessionMessages(authToken string, sessionId uint, req request.AIChatMessagesQueryRequest) (messages []*response.AIChatMessagesResponse, paging paginator.Pagination, err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
// Verify session belongs to user
session, err := _i.Repo.FindSessionByUserAndId(userInfo.ID, sessionId)
if err != nil {
return nil, paginator.Pagination{}, err
}
results, paging, err := _i.Repo.GetSessionMessages(session.SessionID, req)
if err != nil {
return
}
for _, result := range results {
messages = append(messages, mapper.AIChatMessagesResponseMapper(result))
}
return
}
func (_i *aiChatService) SendMessage(authToken string, req request.AIChatMessagesCreateRequest) (message *response.AIChatMessagesResponse, err error) {
// userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
// Verify session exists
session, err := _i.Repo.FindSessionBySessionId(req.SessionID)
if err != nil {
return nil, err
}
entity := req.ToEntity()
result, err := _i.Repo.CreateMessage(entity)
if err != nil {
return nil, err
}
// Increment message count in session
err = _i.Repo.IncrementMessageCount(session.ID)
if err != nil {
_i.Log.Error().Err(err).Msg("Failed to increment message count")
}
return mapper.AIChatMessagesResponseMapper(result), nil
}
// Update Message
func (_i *aiChatService) UpdateMessage(authToken string, messageId uint, req request.AIChatMessagesUpdateRequest) (err error) {
// userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
entity := req.ToEntity()
return _i.Repo.UpdateMessage(messageId, entity)
}
// Delete Message
func (_i *aiChatService) DeleteMessage(authToken string, messageId uint) error {
// userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
return _i.Repo.DeleteMessage(messageId)
}
// Logs methods
func (_i *aiChatService) GetUserLogs(authToken string, req request.AIChatLogsQueryRequest) (logs []*response.AIChatLogsResponse, paging paginator.Pagination, err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
results, paging, err := _i.Repo.GetUserLogs(userInfo.ID, req)
if err != nil {
return
}
for _, result := range results {
logs = append(logs, mapper.AIChatLogsResponseMapper(result))
}
return
}
func (_i *aiChatService) GetLog(authToken string, logId uint) (log *response.AIChatLogsResponse, err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
result, err := _i.Repo.FindLogByUserAndId(userInfo.ID, logId)
if err != nil {
return nil, err
}
return mapper.AIChatLogsResponseMapper(result), nil
}

View File

@ -0,0 +1,54 @@
package article_approvals
import (
"campaign-pool-be/app/module/article_approvals/controller"
"campaign-pool-be/app/module/article_approvals/repository"
"campaign-pool-be/app/module/article_approvals/service"
"github.com/gofiber/fiber/v2"
"go.uber.org/fx"
)
// struct of ArticleApprovalsRouter
type ArticleApprovalsRouter struct {
App fiber.Router
Controller *controller.Controller
}
// register bulky of ArticleApprovals module
var NewArticleApprovalsModule = fx.Options(
// register repository of ArticleApprovals module
fx.Provide(repository.NewArticleApprovalsRepository),
// register service of ArticleApprovals module
fx.Provide(service.NewArticleApprovalsService),
// register controller of ArticleApprovals module
fx.Provide(controller.NewController),
// register router of ArticleApprovals module
fx.Provide(NewArticleApprovalsRouter),
)
// init ArticleApprovalsRouter
func NewArticleApprovalsRouter(fiber *fiber.App, controller *controller.Controller) *ArticleApprovalsRouter {
return &ArticleApprovalsRouter{
App: fiber,
Controller: controller,
}
}
// register routes of ArticleApprovals module
func (_i *ArticleApprovalsRouter) RegisterArticleApprovalsRoutes() {
// define controllers
articleApprovalsController := _i.Controller.ArticleApprovals
// define routes
_i.App.Route("/article-approvals", func(router fiber.Router) {
router.Get("/", articleApprovalsController.All)
router.Get("/:id", articleApprovalsController.Show)
router.Post("/", articleApprovalsController.Save)
router.Put("/:id", articleApprovalsController.Update)
router.Delete("/:id", articleApprovalsController.Delete)
})
}

View File

@ -0,0 +1,202 @@
package controller
import (
"campaign-pool-be/app/module/article_approvals/request"
"campaign-pool-be/app/module/article_approvals/service"
"campaign-pool-be/utils/paginator"
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog"
utilRes "campaign-pool-be/utils/response"
utilVal "campaign-pool-be/utils/validator"
)
type articleApprovalsController struct {
articleApprovalsService service.ArticleApprovalsService
Log zerolog.Logger
}
type ArticleApprovalsController interface {
All(c *fiber.Ctx) error
Show(c *fiber.Ctx) error
Save(c *fiber.Ctx) error
Update(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error
}
func NewArticleApprovalsController(articleApprovalsService service.ArticleApprovalsService, log zerolog.Logger) ArticleApprovalsController {
return &articleApprovalsController{
articleApprovalsService: articleApprovalsService,
Log: log,
}
}
// All get all ArticleApprovals
// @Summary Get all ArticleApprovals
// @Description API for getting all ArticleApprovals
// @Tags ArticleApprovals
// @Security Bearer
// @Param req query request.ArticleApprovalsQueryRequest false "query parameters"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-approvals [get]
func (_i *articleApprovalsController) All(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
reqContext := request.ArticleApprovalsQueryRequestContext{
ArticleId: c.Query("articleId"),
ApprovalBy: c.Query("approvalBy"),
StatusId: c.Query("statusId"),
Message: c.Query("message"),
ApprovalAtLevel: c.Query("approvalAtLevel"),
}
req := reqContext.ToParamRequest()
req.Pagination = paginate
articleApprovalsData, paging, err := _i.articleApprovalsService.All(req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleApprovals list successfully retrieved"},
Data: articleApprovalsData,
Meta: paging,
})
}
// Show get one ArticleApprovals
// @Summary Get one ArticleApprovals
// @Description API for getting one ArticleApprovals
// @Tags ArticleApprovals
// @Security Bearer
// @Param id path int true "ArticleApprovals ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-approvals/{id} [get]
func (_i *articleApprovalsController) Show(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
articleApprovalsData, err := _i.articleApprovalsService.Show(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleApprovals successfully retrieved"},
Data: articleApprovalsData,
})
}
// Save create ArticleApprovals
// @Summary Create ArticleApprovals
// @Description API for create ArticleApprovals
// @Tags ArticleApprovals
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param payload body request.ArticleApprovalsCreateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-approvals [post]
func (_i *articleApprovalsController) Save(c *fiber.Ctx) error {
req := new(request.ArticleApprovalsCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
authToken := c.Get("Authorization")
// Get from context
dataResult, err := _i.articleApprovalsService.Save(*req, authToken)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleApprovals successfully created"},
Data: dataResult,
})
}
// Update update ArticleApprovals
// @Summary update ArticleApprovals
// @Description API for update ArticleApprovals
// @Tags ArticleApprovals
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param payload body request.ArticleApprovalsUpdateRequest true "Required payload"
// @Param id path int true "ArticleApprovals ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-approvals/{id} [put]
func (_i *articleApprovalsController) Update(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
req := new(request.ArticleApprovalsUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err = _i.articleApprovalsService.Update(uint(id), *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleApprovals successfully updated"},
})
}
// Delete delete ArticleApprovals
// @Summary delete ArticleApprovals
// @Description API for delete ArticleApprovals
// @Tags ArticleApprovals
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param id path int true "ArticleApprovals ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-approvals/{id} [delete]
func (_i *articleApprovalsController) Delete(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
err = _i.articleApprovalsService.Delete(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleApprovals successfully deleted"},
})
}

View File

@ -0,0 +1,17 @@
package controller
import (
"campaign-pool-be/app/module/article_approvals/service"
"github.com/rs/zerolog"
)
type Controller struct {
ArticleApprovals ArticleApprovalsController
}
func NewController(ArticleApprovalsService service.ArticleApprovalsService, log zerolog.Logger) *Controller {
return &Controller{
ArticleApprovals: NewArticleApprovalsController(ArticleApprovalsService, log),
}
}

View File

@ -0,0 +1,21 @@
package mapper
import (
"campaign-pool-be/app/database/entity"
res "campaign-pool-be/app/module/article_approvals/response"
)
func ArticleApprovalsResponseMapper(articleApprovalsReq *entity.ArticleApprovals) (articleApprovalsRes *res.ArticleApprovalsResponse) {
if articleApprovalsReq != nil {
articleApprovalsRes = &res.ArticleApprovalsResponse{
ID: articleApprovalsReq.ID,
ArticleId: articleApprovalsReq.ArticleId,
ApprovalBy: articleApprovalsReq.ApprovalBy,
StatusId: articleApprovalsReq.StatusId,
Message: articleApprovalsReq.Message,
ApprovalAtLevel: articleApprovalsReq.ApprovalAtLevel,
CreatedAt: articleApprovalsReq.CreatedAt,
}
}
return articleApprovalsRes
}

View File

@ -0,0 +1,105 @@
package repository
import (
"campaign-pool-be/app/database"
"campaign-pool-be/app/database/entity"
"campaign-pool-be/app/module/article_approvals/request"
"campaign-pool-be/utils/paginator"
utilSvc "campaign-pool-be/utils/service"
"fmt"
"strings"
"github.com/rs/zerolog"
)
type articleApprovalsRepository struct {
DB *database.Database
Log zerolog.Logger
}
// ArticleApprovalsRepository define interface of IArticleApprovalsRepository
type ArticleApprovalsRepository interface {
GetAll(req request.ArticleApprovalsQueryRequest) (articleApprovalss []*entity.ArticleApprovals, paging paginator.Pagination, err error)
FindOne(id uint) (articleApprovals *entity.ArticleApprovals, err error)
Create(articleApprovals *entity.ArticleApprovals) (articleApprovalsReturn *entity.ArticleApprovals, err error)
Update(id uint, articleApprovals *entity.ArticleApprovals) (err error)
Delete(id uint) (err error)
}
func NewArticleApprovalsRepository(db *database.Database, logger zerolog.Logger) ArticleApprovalsRepository {
return &articleApprovalsRepository{
DB: db,
Log: logger,
}
}
// implement interface of IArticleApprovalsRepository
func (_i *articleApprovalsRepository) GetAll(req request.ArticleApprovalsQueryRequest) (articleApprovalss []*entity.ArticleApprovals, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Model(&entity.ArticleApprovals{})
if req.ArticleId != nil {
query = query.Where("article_id = ?", req.ArticleId)
}
if req.ApprovalBy != nil {
query = query.Where("approval_by = ?", req.ApprovalBy)
}
if req.StatusId != nil {
query = query.Where("status_id = ?", req.StatusId)
}
if req.Message != nil && *req.Message != "" {
message := strings.ToLower(*req.Message)
query = query.Where("LOWER(message) LIKE ?", "%"+strings.ToLower(message)+"%")
}
if req.ApprovalAtLevel != nil {
query = query.Where("approval_at_level = ?", req.ApprovalAtLevel)
}
query.Count(&count)
if req.Pagination.SortBy != "" {
direction := "ASC"
if req.Pagination.Sort == "desc" {
direction = "DESC"
}
query.Order(fmt.Sprintf("%s %s", req.Pagination.SortBy, direction))
}
req.Pagination.Count = count
req.Pagination = paginator.Paging(req.Pagination)
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&articleApprovalss).Error
if err != nil {
return
}
paging = *req.Pagination
return
}
func (_i *articleApprovalsRepository) FindOne(id uint) (articleApprovals *entity.ArticleApprovals, err error) {
if err := _i.DB.DB.First(&articleApprovals, id).Error; err != nil {
return nil, err
}
return articleApprovals, nil
}
func (_i *articleApprovalsRepository) Create(articleApprovals *entity.ArticleApprovals) (articleApprovalsReturn *entity.ArticleApprovals, err error) {
result := _i.DB.DB.Create(articleApprovals)
return articleApprovals, result.Error
}
func (_i *articleApprovalsRepository) Update(id uint, articleApprovals *entity.ArticleApprovals) (err error) {
articleApprovalsMap, err := utilSvc.StructToMap(articleApprovals)
if err != nil {
return err
}
return _i.DB.DB.Model(&entity.ArticleApprovals{}).
Where(&entity.ArticleApprovals{ID: id}).
Updates(articleApprovalsMap).Error
}
func (_i *articleApprovalsRepository) Delete(id uint) error {
return _i.DB.DB.Delete(&entity.ArticleApprovals{}, id).Error
}

View File

@ -0,0 +1,92 @@
package request
import (
"campaign-pool-be/app/database/entity"
"campaign-pool-be/utils/paginator"
"strconv"
)
type ArticleApprovalsGeneric interface {
ToEntity()
}
type ArticleApprovalsQueryRequest struct {
ArticleId *int `json:"articleId"`
ApprovalBy *int `json:"approvalBy"`
StatusId *int `json:"statusId"`
Message *string `json:"message"`
ApprovalAtLevel *int `json:"approvalAtLevel"`
Pagination *paginator.Pagination `json:"pagination"`
}
type ArticleApprovalsCreateRequest struct {
ArticleId uint `json:"articleId" validate:"required"`
StatusId int `json:"statusId" validate:"required"`
Message string `json:"message" validate:"required"`
}
func (req ArticleApprovalsCreateRequest) ToEntity() *entity.ArticleApprovals {
return &entity.ArticleApprovals{
ArticleId: req.ArticleId,
StatusId: req.StatusId,
Message: req.Message,
}
}
type ArticleApprovalsUpdateRequest struct {
ID uint `json:"id" validate:"required"`
ArticleId uint `json:"articleId" validate:"required"`
StatusId int `json:"statusId" validate:"required"`
Message string `json:"message" validate:"required"`
}
func (req ArticleApprovalsUpdateRequest) ToEntity() *entity.ArticleApprovals {
return &entity.ArticleApprovals{
ID: req.ID,
ArticleId: req.ArticleId,
StatusId: req.StatusId,
Message: req.Message,
}
}
type ArticleApprovalsQueryRequestContext struct {
ArticleId string `json:"articleId"`
ApprovalBy string `json:"approvalBy"`
StatusId string `json:"statusId"`
Message string `json:"message"`
ApprovalAtLevel string `json:"approvalAtLevel"`
}
func (req ArticleApprovalsQueryRequestContext) ToParamRequest() ArticleApprovalsQueryRequest {
var request ArticleApprovalsQueryRequest
if articleIdStr := req.ArticleId; articleIdStr != "" {
articleId, err := strconv.Atoi(articleIdStr)
if err == nil {
request.ArticleId = &articleId
}
}
if approvalByStr := req.ApprovalBy; approvalByStr != "" {
approvalBy, err := strconv.Atoi(approvalByStr)
if err == nil {
request.ApprovalBy = &approvalBy
}
}
if statusIdStr := req.StatusId; statusIdStr != "" {
statusId, err := strconv.Atoi(statusIdStr)
if err == nil {
request.StatusId = &statusId
}
}
if message := req.Message; message != "" {
request.Message = &message
}
if approvalAtLevelStr := req.ApprovalAtLevel; approvalAtLevelStr != "" {
approvalAtLevel, err := strconv.Atoi(approvalAtLevelStr)
if err == nil {
request.ApprovalAtLevel = &approvalAtLevel
}
}
return request
}

View File

@ -0,0 +1,13 @@
package response
import "time"
type ArticleApprovalsResponse struct {
ID uint `json:"id"`
ArticleId uint `json:"articleId"`
ApprovalBy uint `json:"approvalBy"`
StatusId int `json:"statusId"`
Message string `json:"message"`
ApprovalAtLevel *int `json:"approvalAtLevel"`
CreatedAt time.Time `json:"createdAt"`
}

View File

@ -0,0 +1,96 @@
package service
import (
"campaign-pool-be/app/database/entity"
"campaign-pool-be/app/module/article_approvals/mapper"
"campaign-pool-be/app/module/article_approvals/repository"
"campaign-pool-be/app/module/article_approvals/request"
"campaign-pool-be/app/module/article_approvals/response"
articlesService "campaign-pool-be/app/module/articles/service"
usersRepository "campaign-pool-be/app/module/users/repository"
"campaign-pool-be/utils/paginator"
"github.com/rs/zerolog"
utilSvc "campaign-pool-be/utils/service"
)
// ArticleApprovalsService
type articleApprovalsService struct {
Repo repository.ArticleApprovalsRepository
UsersRepo usersRepository.UsersRepository
ArticlesService articlesService.ArticlesService
Log zerolog.Logger
}
// ArticleApprovalsService define interface of IArticleApprovalsService
type ArticleApprovalsService interface {
All(req request.ArticleApprovalsQueryRequest) (articleApprovals []*response.ArticleApprovalsResponse, paging paginator.Pagination, err error)
Show(id uint) (articleApprovals *response.ArticleApprovalsResponse, err error)
Save(req request.ArticleApprovalsCreateRequest, authToken string) (articleApprovals *entity.ArticleApprovals, err error)
Update(id uint, req request.ArticleApprovalsUpdateRequest) (err error)
Delete(id uint) error
}
// NewArticleApprovalsService init ArticleApprovalsService
func NewArticleApprovalsService(repo repository.ArticleApprovalsRepository, log zerolog.Logger, usersRepo usersRepository.UsersRepository, articlesSvc articlesService.ArticlesService) ArticleApprovalsService {
return &articleApprovalsService{
Repo: repo,
Log: log,
UsersRepo: usersRepo,
ArticlesService: articlesSvc,
}
}
// All implement interface of ArticleApprovalsService
func (_i *articleApprovalsService) All(req request.ArticleApprovalsQueryRequest) (articleApprovalss []*response.ArticleApprovalsResponse, paging paginator.Pagination, err error) {
results, paging, err := _i.Repo.GetAll(req)
if err != nil {
return
}
for _, result := range results {
articleApprovalss = append(articleApprovalss, mapper.ArticleApprovalsResponseMapper(result))
}
return
}
func (_i *articleApprovalsService) Show(id uint) (articleApprovals *response.ArticleApprovalsResponse, err error) {
result, err := _i.Repo.FindOne(id)
if err != nil {
return nil, err
}
return mapper.ArticleApprovalsResponseMapper(result), nil
}
func (_i *articleApprovalsService) Save(req request.ArticleApprovalsCreateRequest, authToken string) (articleApprovals *entity.ArticleApprovals, err error) {
_i.Log.Info().Interface("data", req).Msg("")
newReq := req.ToEntity()
createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
newReq.ApprovalBy = createdBy.ID
newReq.ApprovalAtLevel = &createdBy.UserLevel.LevelNumber
approvalByUserLevelId := createdBy.UserLevelId
approvalParentLevelId := createdBy.UserLevel.ParentLevelId
err = _i.ArticlesService.UpdateApproval(newReq.ArticleId, newReq.StatusId, int(approvalByUserLevelId), *newReq.ApprovalAtLevel, *approvalParentLevelId)
if err != nil {
return nil, err
}
return _i.Repo.Create(newReq)
}
func (_i *articleApprovalsService) Update(id uint, req request.ArticleApprovalsUpdateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("")
return _i.Repo.Update(id, req.ToEntity())
}
func (_i *articleApprovalsService) Delete(id uint) error {
return _i.Repo.Delete(id)
}

View File

@ -0,0 +1,58 @@
package article_categories
import (
"campaign-pool-be/app/module/article_categories/controller"
"campaign-pool-be/app/module/article_categories/repository"
"campaign-pool-be/app/module/article_categories/service"
"github.com/gofiber/fiber/v2"
"go.uber.org/fx"
)
// struct of ArticleCategoriesRouter
type ArticleCategoriesRouter struct {
App fiber.Router
Controller *controller.Controller
}
// register bulky of ArticleCategories module
var NewArticleCategoriesModule = fx.Options(
// register repository of ArticleCategories module
fx.Provide(repository.NewArticleCategoriesRepository),
// register service of ArticleCategories module
fx.Provide(service.NewArticleCategoriesService),
// register controller of ArticleCategories module
fx.Provide(controller.NewController),
// register router of ArticleCategories module
fx.Provide(NewArticleCategoriesRouter),
)
// init ArticleCategoriesRouter
func NewArticleCategoriesRouter(fiber *fiber.App, controller *controller.Controller) *ArticleCategoriesRouter {
return &ArticleCategoriesRouter{
App: fiber,
Controller: controller,
}
}
// register routes of ArticleCategories module
func (_i *ArticleCategoriesRouter) RegisterArticleCategoriesRoutes() {
// define controllers
articleCategoriesController := _i.Controller.ArticleCategories
// define routes
_i.App.Route("/article-categories", func(router fiber.Router) {
router.Get("/", articleCategoriesController.All)
router.Get("/:id", articleCategoriesController.Show)
router.Get("/old/:id", articleCategoriesController.ShowByOldId)
router.Get("/slug/:slug", articleCategoriesController.ShowBySlug)
router.Post("/", articleCategoriesController.Save)
router.Put("/:id", articleCategoriesController.Update)
router.Post("/thumbnail/:id", articleCategoriesController.SaveThumbnail)
router.Get("/thumbnail/viewer/:id", articleCategoriesController.Viewer)
router.Delete("/:id", articleCategoriesController.Delete)
})
}

View File

@ -0,0 +1,298 @@
package controller
import (
"campaign-pool-be/app/module/article_categories/request"
"campaign-pool-be/app/module/article_categories/service"
"campaign-pool-be/utils/paginator"
"strconv"
"github.com/gofiber/fiber/v2"
utilRes "campaign-pool-be/utils/response"
utilVal "campaign-pool-be/utils/validator"
)
type articleCategoriesController struct {
articleCategoriesService service.ArticleCategoriesService
}
type ArticleCategoriesController interface {
All(c *fiber.Ctx) error
Show(c *fiber.Ctx) error
ShowByOldId(c *fiber.Ctx) error
ShowBySlug(c *fiber.Ctx) error
Save(c *fiber.Ctx) error
SaveThumbnail(c *fiber.Ctx) error
Update(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error
Viewer(c *fiber.Ctx) error
}
func NewArticleCategoriesController(articleCategoriesService service.ArticleCategoriesService) ArticleCategoriesController {
return &articleCategoriesController{
articleCategoriesService: articleCategoriesService,
}
}
// All ArticleCategories
// @Summary Get all ArticleCategories
// @Description API for getting all ArticleCategories
// @Tags Article Categories
// @Security Bearer
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param req query request.ArticleCategoriesQueryRequest false "query parameters"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-categories [get]
func (_i *articleCategoriesController) All(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
authToken := c.Get("Authorization")
reqContext := request.ArticleCategoriesQueryRequestContext{
Title: c.Query("title"),
Description: c.Query("description"),
ParentId: c.Query("parentId"),
IsPublish: c.Query("isPublish"),
StatusId: c.Query("statusId"),
}
req := reqContext.ToParamRequest()
req.Pagination = paginate
articleCategoriesData, paging, err := _i.articleCategoriesService.All(req, authToken)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleCategories list successfully retrieved"},
Data: articleCategoriesData,
Meta: paging,
})
}
// Show ArticleCategories
// @Summary Get one ArticleCategories
// @Description API for getting one ArticleCategories
// @Tags Article Categories
// @Security Bearer
// @Param id path int true "ArticleCategories ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-categories/{id} [get]
func (_i *articleCategoriesController) Show(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
articleCategoriesData, err := _i.articleCategoriesService.Show(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleCategories successfully retrieved"},
Data: articleCategoriesData,
})
}
// ShowByOldId ArticleCategories
// @Summary Get one ArticleCategories
// @Description API for getting one ArticleCategories
// @Tags Article Categories
// @Security Bearer
// @Param id path int true "ArticleCategories Old ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-categories/old/{id} [get]
func (_i *articleCategoriesController) ShowByOldId(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
articleCategoriesData, err := _i.articleCategoriesService.ShowByOldId(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleCategories successfully retrieved"},
Data: articleCategoriesData,
})
}
// ShowBySlug ArticleCategories
// @Summary Get one ArticleCategories
// @Description API for getting one ArticleCategories
// @Tags Article Categories
// @Security Bearer
// @Param slug path string true "ArticleCategories Slug"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-categories/slug/{slug} [get]
func (_i *articleCategoriesController) ShowBySlug(c *fiber.Ctx) error {
slug := c.Params("slug")
articleCategoriesData, err := _i.articleCategoriesService.ShowBySlug(slug)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleCategories successfully retrieved"},
Data: articleCategoriesData,
})
}
// Save ArticleCategories
// @Summary Create ArticleCategories
// @Description API for create ArticleCategories
// @Tags Article Categories
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param payload body request.ArticleCategoriesCreateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-categories [post]
func (_i *articleCategoriesController) Save(c *fiber.Ctx) error {
req := new(request.ArticleCategoriesCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
authToken := c.Get("Authorization")
dataResult, err := _i.articleCategoriesService.Save(*req, authToken)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleCategories successfully created"},
Data: dataResult,
})
}
// SaveThumbnail ArticleCategories
// @Summary Upload ArticleCategories Thumbnail
// @Description API for Upload ArticleCategories Thumbnail
// @Tags Article Categories
// @Security Bearer
// @Produce json
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param files formData file true "Upload thumbnail"
// @Param id path int true "ArticleCategories ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-categories/thumbnail/{id} [post]
func (_i *articleCategoriesController) SaveThumbnail(c *fiber.Ctx) error {
err := _i.articleCategoriesService.SaveThumbnail(c)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Thumbnail of ArticleCategories successfully created"},
})
}
// Update ArticleCategories
// @Summary update ArticleCategories
// @Description API for update ArticleCategories
// @Tags Article Categories
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param payload body request.ArticleCategoriesUpdateRequest true "Required payload"
// @Param id path int true "ArticleCategories ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-categories/{id} [put]
func (_i *articleCategoriesController) Update(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
req := new(request.ArticleCategoriesUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err = _i.articleCategoriesService.Update(uint(id), *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleCategories successfully updated"},
})
}
// Delete ArticleCategories
// @Summary delete ArticleCategories
// @Description API for delete ArticleCategories
// @Tags Article Categories
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param id path int true "ArticleCategories ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-categories/{id} [delete]
func (_i *articleCategoriesController) Delete(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
err = _i.articleCategoriesService.Delete(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleCategories successfully deleted"},
})
}
// Viewer ArticleCategories
// @Summary Viewer ArticleCategories
// @Description API for View Thumbnail of ArticleCategories
// @Tags Article Categories
// @Security Bearer
// @Param id path string true "ArticleCategories ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-categories/thumbnail/viewer/{id} [get]
func (_i *articleCategoriesController) Viewer(c *fiber.Ctx) error {
return _i.articleCategoriesService.Viewer(c)
}

View File

@ -0,0 +1,13 @@
package controller
import "campaign-pool-be/app/module/article_categories/service"
type Controller struct {
ArticleCategories ArticleCategoriesController
}
func NewController(ArticleCategoriesService service.ArticleCategoriesService) *Controller {
return &Controller{
ArticleCategories: NewArticleCategoriesController(ArticleCategoriesService),
}
}

View File

@ -0,0 +1,39 @@
package mapper
import (
"campaign-pool-be/app/database/entity"
res "campaign-pool-be/app/module/article_categories/response"
"strconv"
"strings"
)
func ArticleCategoriesResponseMapper(articleCategoriesReq *entity.ArticleCategories, host string) (articleCategoriesRes *res.ArticleCategoriesResponse) {
if articleCategoriesReq != nil {
articleCategoriesRes = &res.ArticleCategoriesResponse{
ID: articleCategoriesReq.ID,
Title: articleCategoriesReq.Title,
Description: articleCategoriesReq.Description,
Slug: articleCategoriesReq.Slug,
ThumbnailPath: articleCategoriesReq.ThumbnailPath,
ParentId: articleCategoriesReq.ParentId,
OldCategoryId: articleCategoriesReq.OldCategoryId,
CreatedById: articleCategoriesReq.CreatedById,
StatusId: articleCategoriesReq.StatusId,
IsPublish: articleCategoriesReq.IsPublish,
PublishedAt: articleCategoriesReq.PublishedAt,
IsActive: articleCategoriesReq.IsActive,
CreatedAt: articleCategoriesReq.CreatedAt,
UpdatedAt: articleCategoriesReq.UpdatedAt,
}
if articleCategoriesReq.Tags != nil {
tagsValue := *articleCategoriesReq.Tags
articleCategoriesRes.Tags = strings.Split(tagsValue, ",")
}
if articleCategoriesRes.ThumbnailPath != nil {
articleCategoriesRes.ThumbnailUrl = host + "/article-categories/thumbnail/viewer/" + strconv.Itoa(int(articleCategoriesReq.ID))
}
}
return articleCategoriesRes
}

View File

@ -0,0 +1,141 @@
package repository
import (
"campaign-pool-be/app/database"
"campaign-pool-be/app/database/entity"
"campaign-pool-be/app/module/article_categories/request"
"campaign-pool-be/utils/paginator"
utilSvc "campaign-pool-be/utils/service"
"fmt"
"strings"
"github.com/rs/zerolog"
)
type articleCategoriesRepository struct {
DB *database.Database
Log zerolog.Logger
}
// ArticleCategoriesRepository define interface of IArticleCategoriesRepository
type ArticleCategoriesRepository interface {
GetAll(req request.ArticleCategoriesQueryRequest) (articleCategoriess []*entity.ArticleCategories, paging paginator.Pagination, err error)
FindOne(id uint) (articleCategories *entity.ArticleCategories, err error)
FindOneByOldId(id uint) (articleCategories *entity.ArticleCategories, err error)
FindOneBySlug(slug string) (articleCategories *entity.ArticleCategories, err error)
Create(articleCategories *entity.ArticleCategories) (articleCategoriesReturn *entity.ArticleCategories, err error)
Update(id uint, articleCategories *entity.ArticleCategories) (err error)
Delete(id uint) (err error)
}
func NewArticleCategoriesRepository(db *database.Database, log zerolog.Logger) ArticleCategoriesRepository {
return &articleCategoriesRepository{
DB: db,
Log: log,
}
}
// implement interface of IArticleCategoriesRepository
func (_i *articleCategoriesRepository) GetAll(req request.ArticleCategoriesQueryRequest) (articleCategoriess []*entity.ArticleCategories, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Model(&entity.ArticleCategories{})
if req.UserLevelId != nil {
query = _i.DB.DB.Model(&entity.ArticleCategories{}).
Joins("LEFT JOIN users ON article_categories.created_by_id = users.id").
Joins("LEFT JOIN user_levels ON users.user_level_id = user_levels.id").
Where("user_levels.id = ? or user_levels.parent_level_id = ?", *req.UserLevelId, *req.UserLevelId)
}
query = query.Where("article_categories.is_active = ?", true)
if req.Title != nil && *req.Title != "" {
title := strings.ToLower(*req.Title)
query = query.Where("LOWER(article_categories.title) LIKE ?", "%"+strings.ToLower(title)+"%")
}
if req.Description != nil && *req.Description != "" {
description := strings.ToLower(*req.Description)
query = query.Where("LOWER(article_categories.description) LIKE ?", "%"+strings.ToLower(description)+"%")
}
if req.ParentId != nil {
query = query.Where("article_categories.parent_id = ?", req.ParentId)
}
if req.IsPublish != nil {
query = query.Where("article_categories.is_publish = ?", req.IsPublish)
}
if req.StatusId != nil {
query = query.Where("article_categories.status_id = ?", req.StatusId)
}
query.Count(&count)
if req.Pagination.SortBy != "" {
direction := "ASC"
if req.Pagination.Sort == "desc" {
direction = "DESC"
}
query.Order(fmt.Sprintf("%s %s", req.Pagination.SortBy, direction))
}
req.Pagination.Count = count
req.Pagination = paginator.Paging(req.Pagination)
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&articleCategoriess).Error
if err != nil {
return
}
paging = *req.Pagination
return
}
func (_i *articleCategoriesRepository) FindOne(id uint) (articleCategories *entity.ArticleCategories, err error) {
query := _i.DB.DB.Where("id = ?", id)
if err := query.First(&articleCategories).Error; err != nil {
return nil, err
}
return articleCategories, nil
}
func (_i *articleCategoriesRepository) FindOneByOldId(id uint) (articleCategories *entity.ArticleCategories, err error) {
query := _i.DB.DB.Where("old_category_id = ?", id)
if err := query.First(&articleCategories).Error; err != nil {
return nil, err
}
return articleCategories, nil
}
func (_i *articleCategoriesRepository) FindOneBySlug(slug string) (articleCategories *entity.ArticleCategories, err error) {
query := _i.DB.DB.Where("slug = ?", slug)
if err := query.First(&articleCategories).Error; err != nil {
return nil, err
}
return articleCategories, nil
}
func (_i *articleCategoriesRepository) Create(articleCategories *entity.ArticleCategories) (articleCategoriesReturn *entity.ArticleCategories, err error) {
result := _i.DB.DB.Create(articleCategories)
return articleCategories, result.Error
}
func (_i *articleCategoriesRepository) Update(id uint, articleCategories *entity.ArticleCategories) (err error) {
articleCategoriesMap, err := utilSvc.StructToMap(articleCategories)
if err != nil {
return err
}
query := _i.DB.DB.Model(&entity.ArticleCategories{}).Where(&entity.ArticleCategories{ID: id})
return query.Updates(articleCategoriesMap).Error
}
func (_i *articleCategoriesRepository) Delete(id uint) error {
query := _i.DB.DB.Model(&entity.ArticleCategories{}).Where("id = ?", id)
return query.Delete(&entity.ArticleCategories{}).Error
}

View File

@ -0,0 +1,128 @@
package request
import (
"campaign-pool-be/app/database/entity"
"campaign-pool-be/utils/paginator"
"strconv"
"time"
)
type ArticleCategoriesGeneric interface {
ToEntity()
}
type ArticleCategoriesQueryRequest struct {
Title *string `json:"title"`
Description *string `json:"description"`
UserLevelId *uint `json:"UserLevelId"`
UserLevelNumber *int `json:"UserLevelNumber"`
ParentId *int `json:"parentId"`
StatusId *int `json:"statusId"`
IsPublish *bool `json:"isPublish"`
Pagination *paginator.Pagination `json:"pagination"`
}
type ArticleCategoriesCreateRequest struct {
Title string `json:"title" validate:"required"`
Description string `json:"description" validate:"required"`
StatusId int `json:"statusId" validate:"required"`
Tags *string `json:"tags"`
Slug *string `json:"slug"`
CreatedById *uint `json:"createdById"`
ParentId *int `json:"parentId"`
OldCategoryId *uint `json:"oldCategoryId"`
}
func (req ArticleCategoriesCreateRequest) ToEntity() *entity.ArticleCategories {
return &entity.ArticleCategories{
Title: req.Title,
Description: req.Description,
Tags: req.Tags,
ParentId: req.ParentId,
Slug: req.Slug,
OldCategoryId: req.OldCategoryId,
StatusId: req.StatusId,
}
}
type ArticleCategoriesUpdateRequest struct {
ID uint `json:"id" validate:"required"`
Title string `json:"title" validate:"required"`
Description string `json:"description" validate:"required"`
StatusId int `json:"statusId" validate:"required"`
Tags *string `json:"tags"`
Slug *string `json:"slug"`
ParentId *int `json:"parentId"`
CreatedById *uint `json:"createdById"`
IsPublish *bool `json:"isPublish"`
PublishedAt *time.Time `json:"publishedAt"`
}
func (req ArticleCategoriesUpdateRequest) ToEntity() *entity.ArticleCategories {
return &entity.ArticleCategories{
ID: req.ID,
Title: req.Title,
Description: req.Description,
ParentId: req.ParentId,
Slug: req.Slug,
Tags: req.Tags,
StatusId: req.StatusId,
IsPublish: req.IsPublish,
PublishedAt: req.PublishedAt,
UpdatedAt: time.Now(),
}
}
type ArticleCategoriesQueryRequestContext struct {
Title string `json:"title"`
Description string `json:"description"`
ParentId string `json:"parentId"`
StatusId string `json:"statusId"`
IsPublish string `json:"isPublish"`
UserLevelId string `json:"UserLevelId"`
UserLevelNumber string `json:"UserLevelNumber"`
}
func (req ArticleCategoriesQueryRequestContext) ToParamRequest() ArticleCategoriesQueryRequest {
var request ArticleCategoriesQueryRequest
if title := req.Title; title != "" {
request.Title = &title
}
if description := req.Description; description != "" {
request.Description = &description
}
if parentIdStr := req.ParentId; parentIdStr != "" {
parentId, err := strconv.Atoi(parentIdStr)
if err == nil {
request.ParentId = &parentId
}
}
if isPublishStr := req.IsPublish; isPublishStr != "" {
isPublish, err := strconv.ParseBool(isPublishStr)
if err == nil {
request.IsPublish = &isPublish
}
}
if statusIdStr := req.StatusId; statusIdStr != "" {
statusId, err := strconv.Atoi(statusIdStr)
if err == nil {
request.StatusId = &statusId
}
}
if userLevelIdStr := req.UserLevelId; userLevelIdStr != "" {
userLevelId, err := strconv.Atoi(userLevelIdStr)
if err == nil {
userLevelIdUint := uint(userLevelId)
request.UserLevelId = &userLevelIdUint
}
}
if userLevelNumberStr := req.UserLevelNumber; userLevelNumberStr != "" {
userLevelNumber, err := strconv.Atoi(userLevelNumberStr)
if err == nil {
request.UserLevelNumber = &userLevelNumber
}
}
return request
}

View File

@ -0,0 +1,23 @@
package response
import "time"
type ArticleCategoriesResponse struct {
ID uint `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
ThumbnailUrl string `json:"thumbnailUrl"`
Slug *string `json:"slug"`
Tags []string `json:"tags"`
ThumbnailPath *string `json:"thumbnailPath"`
ParentId *int `json:"parentId"`
OldCategoryId *uint `json:"oldCategoryId"`
CreatedById *uint `json:"createdById"`
StatusId int `json:"statusId"`
IsPublish *bool `json:"isPublish"`
PublishedAt *time.Time `json:"publishedAt"`
IsEnabled *bool `json:"isEnabled"`
IsActive *bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}

View File

@ -0,0 +1,281 @@
package service
import (
"campaign-pool-be/app/database/entity"
"campaign-pool-be/app/module/article_categories/mapper"
"campaign-pool-be/app/module/article_categories/repository"
"campaign-pool-be/app/module/article_categories/request"
"campaign-pool-be/app/module/article_categories/response"
usersRepository "campaign-pool-be/app/module/users/repository"
config "campaign-pool-be/config/config"
minioStorage "campaign-pool-be/config/config"
"campaign-pool-be/utils/paginator"
utilSvc "campaign-pool-be/utils/service"
"context"
"io"
"log"
"math/rand"
"mime"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/minio/minio-go/v7"
"github.com/rs/zerolog"
)
// ArticleCategoriesService
type articleCategoriesService struct {
Repo repository.ArticleCategoriesRepository
UsersRepo usersRepository.UsersRepository
MinioStorage *minioStorage.MinioStorage
Log zerolog.Logger
Cfg *config.Config
}
// ArticleCategoriesService define interface of IArticleCategoriesService
type ArticleCategoriesService interface {
All(req request.ArticleCategoriesQueryRequest, authToken string) (articleCategories []*response.ArticleCategoriesResponse, paging paginator.Pagination, err error)
Show(id uint) (articleCategories *response.ArticleCategoriesResponse, err error)
ShowByOldId(id uint) (articleCategories *response.ArticleCategoriesResponse, err error)
ShowBySlug(slug string) (articleCategories *response.ArticleCategoriesResponse, err error)
Save(req request.ArticleCategoriesCreateRequest, authToken string) (articleCategories *entity.ArticleCategories, err error)
SaveThumbnail(c *fiber.Ctx) (err error)
Update(id uint, req request.ArticleCategoriesUpdateRequest) (err error)
Delete(id uint) error
Viewer(c *fiber.Ctx) error
}
// NewArticleCategoriesService init ArticleCategoriesService
func NewArticleCategoriesService(repo repository.ArticleCategoriesRepository, usersRepo usersRepository.UsersRepository, minioStorage *minioStorage.MinioStorage, log zerolog.Logger, cfg *config.Config) ArticleCategoriesService {
return &articleCategoriesService{
Repo: repo,
UsersRepo: usersRepo,
MinioStorage: minioStorage,
Log: log,
Cfg: cfg,
}
}
// All implement interface of ArticleCategoriesService
func (_i *articleCategoriesService) All(req request.ArticleCategoriesQueryRequest, authToken string) (articleCategoriess []*response.ArticleCategoriesResponse, paging paginator.Pagination, err error) {
createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
if createdBy != nil {
if createdBy.UserLevel.LevelNumber > 1 {
req.UserLevelId = &createdBy.UserLevelId
}
}
results, paging, err := _i.Repo.GetAll(req)
if err != nil {
return
}
host := _i.Cfg.App.Domain
for _, result := range results {
articleCategoriess = append(articleCategoriess, mapper.ArticleCategoriesResponseMapper(result, host))
}
return
}
func (_i *articleCategoriesService) Show(id uint) (articleCategories *response.ArticleCategoriesResponse, err error) {
result, err := _i.Repo.FindOne(id)
if err != nil {
return nil, err
}
host := _i.Cfg.App.Domain
return mapper.ArticleCategoriesResponseMapper(result, host), nil
}
func (_i *articleCategoriesService) ShowByOldId(id uint) (articleCategories *response.ArticleCategoriesResponse, err error) {
result, err := _i.Repo.FindOneByOldId(id)
if err != nil {
return nil, err
}
host := _i.Cfg.App.Domain
return mapper.ArticleCategoriesResponseMapper(result, host), nil
}
func (_i *articleCategoriesService) ShowBySlug(slug string) (articleCategories *response.ArticleCategoriesResponse, err error) {
result, err := _i.Repo.FindOneBySlug(slug)
if err != nil {
return nil, err
}
host := _i.Cfg.App.Domain
return mapper.ArticleCategoriesResponseMapper(result, host), nil
}
func (_i *articleCategoriesService) Save(req request.ArticleCategoriesCreateRequest, authToken string) (articleCategories *entity.ArticleCategories, err error) {
_i.Log.Info().Interface("data", req).Msg("")
newReq := req.ToEntity()
if req.CreatedById != nil {
createdBy, err := _i.UsersRepo.FindOne(*req.CreatedById)
if err != nil {
return nil, err
}
newReq.CreatedById = &createdBy.ID
} else {
createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
newReq.CreatedById = &createdBy.ID
}
return _i.Repo.Create(newReq)
}
func (_i *articleCategoriesService) SaveThumbnail(c *fiber.Ctx) (err error) {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
form, err := c.MultipartForm()
if err != nil {
return err
}
files := form.File["files"]
// Create minio connection.
minioClient, err := _i.MinioStorage.ConnectMinio()
if err != nil {
// Return status 500 and minio connection error.
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}
// Iterasi semua file yang diunggah
for _, file := range files {
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Uploader:: loop1").
Interface("data", file).Msg("")
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
filename := filepath.Base(file.Filename)
filenameWithoutExt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))])
extension := filepath.Ext(file.Filename)[1:]
rand.New(rand.NewSource(time.Now().UnixNano()))
randUniqueId := rand.Intn(1000000)
newFilenameWithoutExt := filenameWithoutExt + "_" + strconv.Itoa(randUniqueId)
newFilename := newFilenameWithoutExt + "." + extension
objectName := "articles/category/thumbnail/" + newFilename
findCategory, err := _i.Repo.FindOne(uint(id))
findCategory.ThumbnailPath = &objectName
err = _i.Repo.Update(uint(id), findCategory)
if err != nil {
return err
}
// Upload file ke MinIO
_, err = minioClient.PutObject(context.Background(), bucketName, objectName, src, file.Size, minio.PutObjectOptions{})
if err != nil {
return err
}
}
return
}
func (_i *articleCategoriesService) Update(id uint, req request.ArticleCategoriesUpdateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("")
newReq := req.ToEntity()
if req.CreatedById != nil {
createdBy, err := _i.UsersRepo.FindOne(*req.CreatedById)
if err != nil {
return err
}
newReq.CreatedById = &createdBy.ID
}
return _i.Repo.Update(id, newReq)
}
func (_i *articleCategoriesService) Delete(id uint) error {
result, err := _i.Repo.FindOne(id)
if err != nil {
return err
}
isActive := false
result.IsActive = &isActive
return _i.Repo.Update(id, result)
}
func (_i *articleCategoriesService) Viewer(c *fiber.Ctx) (err error) {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
result, err := _i.Repo.FindOne(uint(id))
if err != nil {
return err
}
if result.ThumbnailPath == nil {
return nil
}
ctx := context.Background()
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
objectName := result.ThumbnailPath
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Article:Uploads").
Interface("data", objectName).Msg("")
// Create minio connection.
minioClient, err := _i.MinioStorage.ConnectMinio()
if err != nil {
// Return status 500 and minio connection error.
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}
fileContent, err := minioClient.GetObject(ctx, bucketName, *objectName, minio.GetObjectOptions{})
if err != nil {
log.Fatalln(err)
}
defer fileContent.Close()
contentType := mime.TypeByExtension("." + getFileExtension(*objectName))
if contentType == "" {
contentType = "application/octet-stream"
}
c.Set("Content-Type", contentType)
if _, err := io.Copy(c.Response().BodyWriter(), fileContent); err != nil {
return err
}
return
}
func getFileExtension(filename string) string {
// split file name
parts := strings.Split(filename, ".")
// jika tidak ada ekstensi, kembalikan string kosong
if len(parts) == 1 || (len(parts) == 2 && parts[0] == "") {
return ""
}
// ambil ekstensi terakhir
return parts[len(parts)-1]
}

View File

@ -0,0 +1,54 @@
package article_category_details
import (
"campaign-pool-be/app/module/article_category_details/controller"
"campaign-pool-be/app/module/article_category_details/repository"
"campaign-pool-be/app/module/article_category_details/service"
"github.com/gofiber/fiber/v2"
"go.uber.org/fx"
)
// struct of ArticleCategoryDetailsRouter
type ArticleCategoryDetailsRouter struct {
App fiber.Router
Controller *controller.Controller
}
// register bulky of ArticleCategoryDetails module
var NewArticleCategoryDetailsModule = fx.Options(
// register repository of ArticleCategoryDetails module
fx.Provide(repository.NewArticleCategoryDetailsRepository),
// register service of ArticleCategoryDetails module
fx.Provide(service.NewArticleCategoryDetailsService),
// register controller of ArticleCategoryDetails module
fx.Provide(controller.NewController),
// register router of ArticleCategoryDetails module
fx.Provide(NewArticleCategoryDetailsRouter),
)
// init ArticleCategoryDetailsRouter
func NewArticleCategoryDetailsRouter(fiber *fiber.App, controller *controller.Controller) *ArticleCategoryDetailsRouter {
return &ArticleCategoryDetailsRouter{
App: fiber,
Controller: controller,
}
}
// register routes of ArticleCategoryDetails module
func (_i *ArticleCategoryDetailsRouter) RegisterArticleCategoryDetailsRoutes() {
// define controllers
articleCategoryDetailsController := _i.Controller.ArticleCategoryDetails
// define routes
_i.App.Route("/article-category-details", func(router fiber.Router) {
router.Get("/", articleCategoryDetailsController.All)
router.Get("/:id", articleCategoryDetailsController.Show)
router.Post("/", articleCategoryDetailsController.Save)
router.Put("/:id", articleCategoryDetailsController.Update)
router.Delete("/:id", articleCategoryDetailsController.Delete)
})
}

View File

@ -0,0 +1,185 @@
package controller
import (
"campaign-pool-be/app/module/article_category_details/request"
"campaign-pool-be/app/module/article_category_details/service"
"campaign-pool-be/utils/paginator"
"strconv"
"github.com/gofiber/fiber/v2"
utilRes "campaign-pool-be/utils/response"
utilVal "campaign-pool-be/utils/validator"
)
type articleCategoryDetailsController struct {
articleCategoryDetailsService service.ArticleCategoryDetailsService
}
type ArticleCategoryDetailsController interface {
All(c *fiber.Ctx) error
Show(c *fiber.Ctx) error
Save(c *fiber.Ctx) error
Update(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error
}
func NewArticleCategoryDetailsController(articleCategoryDetailsService service.ArticleCategoryDetailsService) ArticleCategoryDetailsController {
return &articleCategoryDetailsController{
articleCategoryDetailsService: articleCategoryDetailsService,
}
}
// All get all ArticleCategoryDetails
// @Summary Get all ArticleCategoryDetails
// @Description API for getting all ArticleCategoryDetails
// @Tags Untags
// @Security Bearer
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 422 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /article-category-details [get]
func (_i *articleCategoryDetailsController) All(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
var req request.ArticleCategoryDetailsQueryRequest
req.Pagination = paginate
articleCategoryDetailsData, paging, err := _i.articleCategoryDetailsService.All(req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Messages: utilRes.Messages{"ArticleCategoryDetails list successfully retrieved"},
Data: articleCategoryDetailsData,
Meta: paging,
})
}
// Show get one ArticleCategoryDetails
// @Summary Get one ArticleCategoryDetails
// @Description API for getting one ArticleCategoryDetails
// @Tags Untags
// @Security Bearer
// @Param id path int true "ArticleCategoryDetails ID"
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 422 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /article-category-details/{id} [get]
func (_i *articleCategoryDetailsController) Show(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
articleCategoryDetailsData, err := _i.articleCategoryDetailsService.Show(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Messages: utilRes.Messages{"ArticleCategoryDetails successfully retrieved"},
Data: articleCategoryDetailsData,
})
}
// Save create ArticleCategoryDetails
// @Summary Create ArticleCategoryDetails
// @Description API for create ArticleCategoryDetails
// @Tags Untags
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Body request.ArticleCategoryDetailsCreateRequest
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 422 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /article-category-details [post]
func (_i *articleCategoryDetailsController) Save(c *fiber.Ctx) error {
req := new(request.ArticleCategoryDetailsCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err := _i.articleCategoryDetailsService.Save(*req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Messages: utilRes.Messages{"ArticleCategoryDetails successfully created"},
})
}
// Update update ArticleCategoryDetails
// @Summary update ArticleCategoryDetails
// @Description API for update ArticleCategoryDetails
// @Tags Untags
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Body request.ArticleCategoryDetailsUpdateRequest
// @Param id path int true "ArticleCategoryDetails ID"
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 422 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /article-category-details/{id} [put]
func (_i *articleCategoryDetailsController) Update(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
req := new(request.ArticleCategoryDetailsUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err = _i.articleCategoryDetailsService.Update(uint(id), *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Messages: utilRes.Messages{"ArticleCategoryDetails successfully updated"},
})
}
// Delete delete ArticleCategoryDetails
// @Summary delete ArticleCategoryDetails
// @Description API for delete ArticleCategoryDetails
// @Tags Untags
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param id path int true "ArticleCategoryDetails ID"
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 422 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /article-category-details/{id} [delete]
func (_i *articleCategoryDetailsController) Delete(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
err = _i.articleCategoryDetailsService.Delete(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Messages: utilRes.Messages{"ArticleCategoryDetails successfully deleted"},
})
}

View File

@ -0,0 +1,13 @@
package controller
import "campaign-pool-be/app/module/article_category_details/service"
type Controller struct {
ArticleCategoryDetails ArticleCategoryDetailsController
}
func NewController(ArticleCategoryDetailsService service.ArticleCategoryDetailsService) *Controller {
return &Controller{
ArticleCategoryDetails: NewArticleCategoryDetailsController(ArticleCategoryDetailsService),
}
}

View File

@ -0,0 +1,21 @@
package mapper
import (
"campaign-pool-be/app/database/entity/article_category_details"
res "campaign-pool-be/app/module/article_category_details/response"
)
func ArticleCategoryDetailsResponseMapper(articleCategoryDetailsReq *article_category_details.ArticleCategoryDetails) (articleCategoryDetailsRes *res.ArticleCategoryDetailsResponse) {
if articleCategoryDetailsReq != nil {
articleCategoryDetailsRes = &res.ArticleCategoryDetailsResponse{
ID: articleCategoryDetailsReq.ID,
ArticleId: articleCategoryDetailsReq.ArticleId,
CategoryId: articleCategoryDetailsReq.CategoryId,
IsActive: articleCategoryDetailsReq.IsActive,
CreatedAt: articleCategoryDetailsReq.CreatedAt,
UpdatedAt: articleCategoryDetailsReq.UpdatedAt,
}
}
return articleCategoryDetailsRes
}

View File

@ -0,0 +1,78 @@
package repository
import (
"campaign-pool-be/app/database"
"campaign-pool-be/app/database/entity/article_category_details"
"campaign-pool-be/app/module/article_category_details/request"
"campaign-pool-be/utils/paginator"
)
type articleCategoryDetailsRepository struct {
DB *database.Database
}
// ArticleCategoryDetailsRepository define interface of IArticleCategoryDetailsRepository
type ArticleCategoryDetailsRepository interface {
GetAll(req request.ArticleCategoryDetailsQueryRequest) (articleCategoryDetailss []*article_category_details.ArticleCategoryDetails, paging paginator.Pagination, err error)
FindOne(id uint) (articleCategoryDetails *article_category_details.ArticleCategoryDetails, err error)
FindByArticleId(articleId uint) (articleCategoryDetailss []*article_category_details.ArticleCategoryDetails, err error)
Create(articleCategoryDetails *article_category_details.ArticleCategoryDetails) (err error)
Update(id uint, articleCategoryDetails *article_category_details.ArticleCategoryDetails) (err error)
Delete(id uint) (err error)
}
func NewArticleCategoryDetailsRepository(db *database.Database) ArticleCategoryDetailsRepository {
return &articleCategoryDetailsRepository{
DB: db,
}
}
// implement interface of IArticleCategoryDetailsRepository
func (_i *articleCategoryDetailsRepository) GetAll(req request.ArticleCategoryDetailsQueryRequest) (articleCategoryDetailss []*article_category_details.ArticleCategoryDetails, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Model(&article_category_details.ArticleCategoryDetails{})
query.Count(&count)
req.Pagination.Count = count
req.Pagination = paginator.Paging(req.Pagination)
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&articleCategoryDetailss).Error
if err != nil {
return
}
paging = *req.Pagination
return
}
func (_i *articleCategoryDetailsRepository) FindOne(id uint) (articleCategoryDetails *article_category_details.ArticleCategoryDetails, err error) {
if err := _i.DB.DB.First(&articleCategoryDetails, id).Error; err != nil {
return nil, err
}
return articleCategoryDetails, nil
}
func (_i *articleCategoryDetailsRepository) FindByArticleId(articleId uint) (articleCategoryDetailss []*article_category_details.ArticleCategoryDetails, err error) {
if err := _i.DB.DB.Where("article_id = ?", articleId).Preload("Category").Find(&articleCategoryDetailss).Error; err != nil {
return nil, err
}
return articleCategoryDetailss, nil
}
func (_i *articleCategoryDetailsRepository) Create(articleCategoryDetails *article_category_details.ArticleCategoryDetails) (err error) {
return _i.DB.DB.Create(articleCategoryDetails).Error
}
func (_i *articleCategoryDetailsRepository) Update(id uint, articleCategoryDetails *article_category_details.ArticleCategoryDetails) (err error) {
return _i.DB.DB.Model(&article_category_details.ArticleCategoryDetails{}).
Where(&article_category_details.ArticleCategoryDetails{ID: id}).
Updates(articleCategoryDetails).Error
}
func (_i *articleCategoryDetailsRepository) Delete(id uint) error {
return _i.DB.DB.Delete(&article_category_details.ArticleCategoryDetails{}, id).Error
}

View File

@ -0,0 +1,52 @@
package request
import (
"campaign-pool-be/app/database/entity/article_category_details"
"campaign-pool-be/utils/paginator"
"time"
)
type ArticleCategoryDetailsGeneric interface {
ToEntity()
}
type ArticleCategoryDetailsQueryRequest struct {
ArticleId int `json:"articleId" validate:"required"`
CategoryId int `json:"categoryId" validate:"required"`
IsActive bool `json:"isActive" validate:"required"`
Pagination *paginator.Pagination `json:"pagination"`
}
type ArticleCategoryDetailsCreateRequest struct {
ArticleId uint `json:"articleId" validate:"required"`
CategoryId int `json:"categoryId" validate:"required"`
IsActive bool `json:"isActive" validate:"required"`
}
func (req ArticleCategoryDetailsCreateRequest) ToEntity() *article_category_details.ArticleCategoryDetails {
return &article_category_details.ArticleCategoryDetails{
ArticleId: req.ArticleId,
CategoryId: req.CategoryId,
IsActive: req.IsActive,
}
}
type ArticleCategoryDetailsUpdateRequest struct {
ID uint `json:"id" validate:"required"`
ArticleId uint `json:"articleId" validate:"required"`
CategoryId int `json:"categoryId" validate:"required"`
IsActive bool `json:"isActive" validate:"required"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (req ArticleCategoryDetailsUpdateRequest) ToEntity() *article_category_details.ArticleCategoryDetails {
return &article_category_details.ArticleCategoryDetails{
ID: req.ID,
ArticleId: req.ArticleId,
CategoryId: req.CategoryId,
IsActive: req.IsActive,
CreatedAt: req.CreatedAt,
UpdatedAt: req.UpdatedAt,
}
}

View File

@ -0,0 +1,12 @@
package response
import "time"
type ArticleCategoryDetailsResponse struct {
ID uint `json:"id"`
ArticleId uint `json:"articleId"`
CategoryId int `json:"categoryId"`
IsActive bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}

View File

@ -0,0 +1,73 @@
package service
import (
"campaign-pool-be/app/module/article_category_details/mapper"
"campaign-pool-be/app/module/article_category_details/repository"
"campaign-pool-be/app/module/article_category_details/request"
"campaign-pool-be/app/module/article_category_details/response"
"campaign-pool-be/utils/paginator"
"github.com/rs/zerolog"
)
// ArticleCategoryDetailsService
type articleCategoryDetailsService struct {
Repo repository.ArticleCategoryDetailsRepository
Log zerolog.Logger
}
// ArticleCategoryDetailsService define interface of IArticleCategoryDetailsService
type ArticleCategoryDetailsService interface {
All(req request.ArticleCategoryDetailsQueryRequest) (articleCategoryDetails []*response.ArticleCategoryDetailsResponse, paging paginator.Pagination, err error)
Show(id uint) (articleCategoryDetails *response.ArticleCategoryDetailsResponse, err error)
Save(req request.ArticleCategoryDetailsCreateRequest) (err error)
Update(id uint, req request.ArticleCategoryDetailsUpdateRequest) (err error)
Delete(id uint) error
}
// NewArticleCategoryDetailsService init ArticleCategoryDetailsService
func NewArticleCategoryDetailsService(repo repository.ArticleCategoryDetailsRepository, log zerolog.Logger) ArticleCategoryDetailsService {
return &articleCategoryDetailsService{
Repo: repo,
Log: log,
}
}
// All implement interface of ArticleCategoryDetailsService
func (_i *articleCategoryDetailsService) All(req request.ArticleCategoryDetailsQueryRequest) (articleCategoryDetailss []*response.ArticleCategoryDetailsResponse, paging paginator.Pagination, err error) {
results, paging, err := _i.Repo.GetAll(req)
if err != nil {
return
}
for _, result := range results {
articleCategoryDetailss = append(articleCategoryDetailss, mapper.ArticleCategoryDetailsResponseMapper(result))
}
return
}
func (_i *articleCategoryDetailsService) Show(id uint) (articleCategoryDetails *response.ArticleCategoryDetailsResponse, err error) {
result, err := _i.Repo.FindOne(id)
if err != nil {
return nil, err
}
return mapper.ArticleCategoryDetailsResponseMapper(result), nil
}
func (_i *articleCategoryDetailsService) Save(req request.ArticleCategoryDetailsCreateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("")
return _i.Repo.Create(req.ToEntity())
}
func (_i *articleCategoryDetailsService) Update(id uint, req request.ArticleCategoryDetailsUpdateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("")
return _i.Repo.Update(id, req.ToEntity())
}
func (_i *articleCategoryDetailsService) Delete(id uint) error {
return _i.Repo.Delete(id)
}

View File

@ -0,0 +1,54 @@
package article_comments
import (
"campaign-pool-be/app/module/article_comments/controller"
"campaign-pool-be/app/module/article_comments/repository"
"campaign-pool-be/app/module/article_comments/service"
"github.com/gofiber/fiber/v2"
"go.uber.org/fx"
)
// struct of ArticleCommentsRouter
type ArticleCommentsRouter struct {
App fiber.Router
Controller *controller.Controller
}
// register bulky of ArticleComments module
var NewArticleCommentsModule = fx.Options(
// register repository of ArticleComments module
fx.Provide(repository.NewArticleCommentsRepository),
// register service of ArticleComments module
fx.Provide(service.NewArticleCommentsService),
// register controller of ArticleComments module
fx.Provide(controller.NewController),
// register router of ArticleComments module
fx.Provide(NewArticleCommentsRouter),
)
// init ArticleCommentsRouter
func NewArticleCommentsRouter(fiber *fiber.App, controller *controller.Controller) *ArticleCommentsRouter {
return &ArticleCommentsRouter{
App: fiber,
Controller: controller,
}
}
// register routes of ArticleComments module
func (_i *ArticleCommentsRouter) RegisterArticleCommentsRoutes() {
// define controllers
articleCommentsController := _i.Controller.ArticleComments
// define routes
_i.App.Route("/article-comments", func(router fiber.Router) {
router.Get("/", articleCommentsController.All)
router.Get("/:id", articleCommentsController.Show)
router.Post("/", articleCommentsController.Save)
router.Put("/:id", articleCommentsController.Update)
router.Delete("/:id", articleCommentsController.Delete)
router.Post("/approval", articleCommentsController.Approval)
})
}

View File

@ -0,0 +1,235 @@
package controller
import (
"campaign-pool-be/app/module/article_comments/request"
"campaign-pool-be/app/module/article_comments/service"
"campaign-pool-be/utils/paginator"
utilRes "campaign-pool-be/utils/response"
utilVal "campaign-pool-be/utils/validator"
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog"
)
type articleCommentsController struct {
articleCommentsService service.ArticleCommentsService
Log zerolog.Logger
}
type ArticleCommentsController interface {
All(c *fiber.Ctx) error
Show(c *fiber.Ctx) error
Save(c *fiber.Ctx) error
Update(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error
Approval(c *fiber.Ctx) error
}
func NewArticleCommentsController(articleCommentsService service.ArticleCommentsService, log zerolog.Logger) ArticleCommentsController {
return &articleCommentsController{
articleCommentsService: articleCommentsService,
Log: log,
}
}
// All get all ArticleComments
// @Summary Get all ArticleComments
// @Description API for getting all ArticleComments
// @Tags ArticleComments
// @Security Bearer
// @Param req query request.ArticleCommentsQueryRequest false "query parameters"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-comments [get]
func (_i *articleCommentsController) All(c *fiber.Ctx) error {
// Get from context
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
reqContext := request.ArticleCommentsQueryRequestContext{
Message: c.Query("message"),
ArticleId: c.Query("articleId"),
CommentFrom: c.Query("commentFrom"),
ParentId: c.Query("parentId"),
IsPublic: c.Query("isPublic"),
}
req := reqContext.ToParamRequest()
req.Pagination = paginate
articleCommentsData, paging, err := _i.articleCommentsService.All(req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleComments list successfully retrieved"},
Data: articleCommentsData,
Meta: paging,
})
}
// Show get one ArticleComments
// @Summary Get one ArticleComments
// @Description API for getting one ArticleComments
// @Tags ArticleComments
// @Security Bearer
// @Param id path int true "ArticleComments ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-comments/{id} [get]
func (_i *articleCommentsController) Show(c *fiber.Ctx) error {
// Get from context
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
articleCommentsData, err := _i.articleCommentsService.Show(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleComments successfully retrieved"},
Data: articleCommentsData,
})
}
// Save create ArticleComments
// @Summary Create ArticleComments
// @Description API for create ArticleComments
// @Tags ArticleComments
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param payload body request.ArticleCommentsCreateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-comments [post]
func (_i *articleCommentsController) Save(c *fiber.Ctx) error {
// Get from context
req := new(request.ArticleCommentsCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
authToken := c.Get("Authorization")
dataResult, err := _i.articleCommentsService.Save(*req, authToken)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleComments successfully created"},
Data: dataResult,
})
}
// Update update ArticleComments
// @Summary update ArticleComments
// @Description API for update ArticleComments
// @Tags ArticleComments
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param payload body request.ArticleCommentsUpdateRequest true "Required payload"
// @Param id path int true "ArticleComments ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-comments/{id} [put]
func (_i *articleCommentsController) Update(c *fiber.Ctx) error {
// Get from context
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
req := new(request.ArticleCommentsUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err = _i.articleCommentsService.Update(uint(id), *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleComments successfully updated"},
})
}
// Delete delete ArticleComments
// @Summary delete ArticleComments
// @Description API for delete ArticleComments
// @Tags ArticleComments
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param id path int true "ArticleComments ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-comments/{id} [delete]
func (_i *articleCommentsController) Delete(c *fiber.Ctx) error {
// Get from context
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
err = _i.articleCommentsService.Delete(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleComments successfully deleted"},
})
}
// Approval ArticleComments
// @Summary Approval ArticleComments
// @Description API for Approval ArticleComments
// @Tags ArticleComments
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param payload body request.ArticleCommentsApprovalRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-comments/approval [post]
func (_i *articleCommentsController) Approval(c *fiber.Ctx) error {
// Get from context
req := new(request.ArticleCommentsApprovalRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err := _i.articleCommentsService.Approval(req.ID, *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleComments successfully reviewed"},
})
}

View File

@ -0,0 +1,17 @@
package controller
import (
"campaign-pool-be/app/module/article_comments/service"
"github.com/rs/zerolog"
)
type Controller struct {
ArticleComments ArticleCommentsController
}
func NewController(ArticleCommentsService service.ArticleCommentsService, log zerolog.Logger) *Controller {
return &Controller{
ArticleComments: NewArticleCommentsController(ArticleCommentsService, log),
}
}

View File

@ -0,0 +1,35 @@
package mapper
import (
"campaign-pool-be/app/database/entity"
res "campaign-pool-be/app/module/article_comments/response"
usersRepository "campaign-pool-be/app/module/users/repository"
)
func ArticleCommentsResponseMapper(articleCommentsReq *entity.ArticleComments, usersRepo usersRepository.UsersRepository) (articleCommentsRes *res.ArticleCommentsResponse) {
if articleCommentsReq != nil {
commentFromName := ""
if articleCommentsReq.CommentFrom != nil {
findUser, _ := usersRepo.FindOne(*articleCommentsReq.CommentFrom)
if findUser != nil {
commentFromName = findUser.Fullname
}
}
articleCommentsRes = &res.ArticleCommentsResponse{
ID: articleCommentsReq.ID,
Message: articleCommentsReq.Message,
ArticleId: articleCommentsReq.ArticleId,
CommentFromId: articleCommentsReq.CommentFrom,
CommentFromName: &commentFromName,
ParentId: articleCommentsReq.ParentId,
IsPublic: articleCommentsReq.IsPublic,
StatusId: articleCommentsReq.StatusId,
IsActive: articleCommentsReq.IsActive,
CreatedAt: articleCommentsReq.CreatedAt,
UpdatedAt: articleCommentsReq.UpdatedAt,
}
}
return articleCommentsRes
}

View File

@ -0,0 +1,108 @@
package repository
import (
"campaign-pool-be/app/database"
"campaign-pool-be/app/database/entity"
"campaign-pool-be/app/module/article_comments/request"
"campaign-pool-be/utils/paginator"
utilSvc "campaign-pool-be/utils/service"
"fmt"
"strings"
"github.com/rs/zerolog"
)
type articleCommentsRepository struct {
DB *database.Database
Log zerolog.Logger
}
// ArticleCommentsRepository define interface of IArticleCommentsRepository
type ArticleCommentsRepository interface {
GetAll(req request.ArticleCommentsQueryRequest) (articleCommentss []*entity.ArticleComments, paging paginator.Pagination, err error)
FindOne(id uint) (articleComments *entity.ArticleComments, err error)
Create(articleComments *entity.ArticleComments) (articleCommentsReturn *entity.ArticleComments, err error)
Update(id uint, articleComments *entity.ArticleComments) (err error)
Delete(id uint) (err error)
}
func NewArticleCommentsRepository(db *database.Database, logger zerolog.Logger) ArticleCommentsRepository {
return &articleCommentsRepository{
DB: db,
Log: logger,
}
}
// implement interface of IArticleCommentsRepository
func (_i *articleCommentsRepository) GetAll(req request.ArticleCommentsQueryRequest) (articleCommentss []*entity.ArticleComments, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Model(&entity.ArticleComments{})
query = query.Where("is_active = ?", true)
if req.Message != nil && *req.Message != "" {
message := strings.ToLower(*req.Message)
query = query.Where("LOWER(message) LIKE ?", "%"+strings.ToLower(message)+"%")
}
if req.ArticleId != nil {
query = query.Where("article_id = ?", req.ArticleId)
}
if req.CommentFrom != nil {
query = query.Where("comment_from = ?", req.CommentFrom)
}
if req.ParentId != nil {
query = query.Where("parent_id = ?", req.ParentId)
}
if req.IsPublic != nil {
query = query.Where("is_public = ?", req.IsPublic)
}
query.Count(&count)
if req.Pagination.SortBy != "" {
direction := "ASC"
if req.Pagination.Sort == "desc" {
direction = "DESC"
}
query.Order(fmt.Sprintf("%s %s", req.Pagination.SortBy, direction))
}
req.Pagination.Count = count
req.Pagination = paginator.Paging(req.Pagination)
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&articleCommentss).Error
if err != nil {
return
}
paging = *req.Pagination
return
}
func (_i *articleCommentsRepository) FindOne(id uint) (articleComments *entity.ArticleComments, err error) {
query := _i.DB.DB
if err := query.First(&articleComments, id).Error; err != nil {
return nil, err
}
return articleComments, nil
}
func (_i *articleCommentsRepository) Create(articleComments *entity.ArticleComments) (articleCommentsReturn *entity.ArticleComments, err error) {
result := _i.DB.DB.Create(articleComments)
return articleComments, result.Error
}
func (_i *articleCommentsRepository) Update(id uint, articleComments *entity.ArticleComments) (err error) {
articleCommentsMap, err := utilSvc.StructToMap(articleComments)
if err != nil {
return err
}
return _i.DB.DB.Model(&entity.ArticleComments{}).
Where(&entity.ArticleComments{ID: id}).
Updates(articleCommentsMap).Error
}
func (_i *articleCommentsRepository) Delete(id uint) error {
return _i.DB.DB.Delete(&entity.ArticleComments{}, id).Error
}

View File

@ -0,0 +1,111 @@
package request
import (
"campaign-pool-be/app/database/entity"
"campaign-pool-be/utils/paginator"
"strconv"
"time"
)
type ArticleCommentsGeneric interface {
ToEntity()
}
type ArticleCommentsQueryRequest struct {
Message *string `json:"message"`
ArticleId *int `json:"articleId"`
CommentFrom *int `json:"commentFrom"`
ParentId *int `json:"parentId"`
IsPublic *bool `json:"isPublic"`
Pagination *paginator.Pagination `json:"pagination"`
}
type ArticleCommentsCreateRequest struct {
Message string `json:"message" validate:"required"`
ArticleId uint `json:"articleId" validate:"required"`
ParentId *int `json:"parentId"`
IsPublic *bool `json:"isPublic"`
}
func (req ArticleCommentsCreateRequest) ToEntity() *entity.ArticleComments {
return &entity.ArticleComments{
Message: req.Message,
ArticleId: req.ArticleId,
ParentId: req.ParentId,
StatusId: 0,
}
}
type ArticleCommentsUpdateRequest struct {
ID uint `json:"id" validate:"required"`
Message string `json:"message" validate:"required"`
ArticleId uint `json:"articleId" validate:"required"`
ParentId *int `json:"parentId"`
IsPublic *bool `json:"isPublic"`
}
func (req ArticleCommentsUpdateRequest) ToEntity() *entity.ArticleComments {
return &entity.ArticleComments{
ID: req.ID,
Message: req.Message,
ArticleId: req.ArticleId,
ParentId: req.ParentId,
UpdatedAt: time.Now(),
}
}
type ArticleCommentsApprovalRequest struct {
ID uint `json:"id" validate:"required"`
StatusId int `json:"statusId" validate:"required"`
}
func (req ArticleCommentsApprovalRequest) ToEntity() *entity.ArticleComments {
approvedNow := time.Now()
return &entity.ArticleComments{
ID: req.ID,
StatusId: req.StatusId,
ApprovedAt: &approvedNow,
}
}
type ArticleCommentsQueryRequestContext struct {
Message string `json:"message"`
ArticleId string `json:"articleId"`
CommentFrom string `json:"commentFrom"`
ParentId string `json:"parentId"`
IsPublic string `json:"isPublic"`
}
func (req ArticleCommentsQueryRequestContext) ToParamRequest() ArticleCommentsQueryRequest {
var request ArticleCommentsQueryRequest
if message := req.Message; message != "" {
request.Message = &message
}
if articleIdStr := req.ArticleId; articleIdStr != "" {
articleId, err := strconv.Atoi(articleIdStr)
if err == nil {
request.ArticleId = &articleId
}
}
if commentFromStr := req.CommentFrom; commentFromStr != "" {
commentFrom, err := strconv.Atoi(commentFromStr)
if err == nil {
request.CommentFrom = &commentFrom
}
}
if parentIdStr := req.ParentId; parentIdStr != "" {
parentId, err := strconv.Atoi(parentIdStr)
if err == nil {
request.ParentId = &parentId
}
}
if isPublicStr := req.IsPublic; isPublicStr != "" {
isPublic, err := strconv.ParseBool(isPublicStr)
if err == nil {
request.IsPublic = &isPublic
}
}
return request
}

View File

@ -0,0 +1,17 @@
package response
import "time"
type ArticleCommentsResponse struct {
ID uint `json:"id"`
Message string `json:"message"`
ArticleId uint `json:"articleId"`
CommentFromId *uint `json:"commentFromId"`
CommentFromName *string `json:"commentFromName"`
ParentId *int `json:"parentId"`
StatusId int `json:"statusId"`
IsPublic bool `json:"isPublic"`
IsActive bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}

View File

@ -0,0 +1,105 @@
package service
import (
"campaign-pool-be/app/database/entity"
"campaign-pool-be/app/module/article_comments/mapper"
"campaign-pool-be/app/module/article_comments/repository"
"campaign-pool-be/app/module/article_comments/request"
"campaign-pool-be/app/module/article_comments/response"
usersRepository "campaign-pool-be/app/module/users/repository"
"campaign-pool-be/utils/paginator"
"github.com/rs/zerolog"
utilSvc "campaign-pool-be/utils/service"
)
// ArticleCommentsService
type articleCommentsService struct {
Repo repository.ArticleCommentsRepository
UsersRepo usersRepository.UsersRepository
Log zerolog.Logger
}
// ArticleCommentsService define interface of IArticleCommentsService
type ArticleCommentsService interface {
All(req request.ArticleCommentsQueryRequest) (articleComments []*response.ArticleCommentsResponse, paging paginator.Pagination, err error)
Show(id uint) (articleComments *response.ArticleCommentsResponse, err error)
Save(req request.ArticleCommentsCreateRequest, authToken string) (articleComments *entity.ArticleComments, err error)
Update(id uint, req request.ArticleCommentsUpdateRequest) (err error)
Delete(id uint) error
Approval(id uint, req request.ArticleCommentsApprovalRequest) (err error)
}
// NewArticleCommentsService init ArticleCommentsService
func NewArticleCommentsService(repo repository.ArticleCommentsRepository, log zerolog.Logger, usersRepo usersRepository.UsersRepository) ArticleCommentsService {
return &articleCommentsService{
Repo: repo,
Log: log,
UsersRepo: usersRepo,
}
}
// All implement interface of ArticleCommentsService
func (_i *articleCommentsService) All(req request.ArticleCommentsQueryRequest) (articleCommentss []*response.ArticleCommentsResponse, paging paginator.Pagination, err error) {
results, paging, err := _i.Repo.GetAll(req)
if err != nil {
return
}
for _, result := range results {
articleCommentss = append(articleCommentss, mapper.ArticleCommentsResponseMapper(result, _i.UsersRepo))
}
return
}
func (_i *articleCommentsService) Show(id uint) (articleComments *response.ArticleCommentsResponse, err error) {
result, err := _i.Repo.FindOne(id)
if err != nil {
return nil, err
}
return mapper.ArticleCommentsResponseMapper(result, _i.UsersRepo), nil
}
func (_i *articleCommentsService) Save(req request.ArticleCommentsCreateRequest, authToken string) (articleComments *entity.ArticleComments, err error) {
_i.Log.Info().Interface("data", req).Msg("")
newReq := req.ToEntity()
createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
newReq.CommentFrom = &createdBy.ID
newReq.IsActive = true
return _i.Repo.Create(newReq)
}
func (_i *articleCommentsService) Update(id uint, req request.ArticleCommentsUpdateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("")
return _i.Repo.Update(id, req.ToEntity())
}
func (_i *articleCommentsService) Delete(id uint) error {
result, err := _i.Repo.FindOne(id)
if err != nil {
return err
}
result.IsActive = false
return _i.Repo.Update(id, result)
}
func (_i *articleCommentsService) Approval(id uint, req request.ArticleCommentsApprovalRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("")
newReq := req.ToEntity()
if newReq.StatusId == 1 || newReq.StatusId == 2 {
newReq.IsPublic = true
} else {
newReq.IsPublic = false
}
return _i.Repo.Update(id, newReq)
}

View File

@ -0,0 +1,58 @@
package article_files
import (
"campaign-pool-be/app/module/article_files/controller"
"campaign-pool-be/app/module/article_files/repository"
"campaign-pool-be/app/module/article_files/service"
"github.com/gofiber/fiber/v2"
"go.uber.org/fx"
)
// struct of ArticleFilesRouter
type ArticleFilesRouter struct {
App fiber.Router
Controller *controller.Controller
}
// register bulky of ArticleFiles module
var NewArticleFilesModule = fx.Options(
// register repository of ArticleFiles module
fx.Provide(repository.NewArticleFilesRepository),
// register service of ArticleFiles module
fx.Provide(service.NewArticleFilesService),
fx.Provide(service.NewUploadService),
fx.Provide(service.NewUploadManager),
// register controller of ArticleFiles module
fx.Provide(controller.NewController),
// register router of ArticleFiles module
fx.Provide(NewArticleFilesRouter),
)
// init ArticleFilesRouter
func NewArticleFilesRouter(fiber *fiber.App, controller *controller.Controller) *ArticleFilesRouter {
return &ArticleFilesRouter{
App: fiber,
Controller: controller,
}
}
// register routes of ArticleFiles module
func (_i *ArticleFilesRouter) RegisterArticleFilesRoutes() {
// define controllers
articleFilesController := _i.Controller.ArticleFiles
// define routes
_i.App.Route("/article-files", func(router fiber.Router) {
router.Get("/", articleFilesController.All)
router.Get("/:id", articleFilesController.Show)
router.Post("/:articleId", articleFilesController.Save)
router.Put("/:id", articleFilesController.Update)
router.Delete("/:id", articleFilesController.Delete)
router.Get("/viewer/:filename", articleFilesController.Viewer)
router.Get("/upload-status/:uploadId", articleFilesController.GetUploadStatus)
})
}

View File

@ -0,0 +1,240 @@
package controller
import (
"campaign-pool-be/app/module/article_files/request"
"campaign-pool-be/app/module/article_files/service"
"campaign-pool-be/utils/paginator"
utilRes "campaign-pool-be/utils/response"
utilVal "campaign-pool-be/utils/validator"
"fmt"
"strconv"
"github.com/gofiber/fiber/v2"
)
type articleFilesController struct {
articleFilesService service.ArticleFilesService
}
type ArticleFilesController interface {
All(c *fiber.Ctx) error
Show(c *fiber.Ctx) error
Save(c *fiber.Ctx) error
Update(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error
Viewer(c *fiber.Ctx) error
GetUploadStatus(c *fiber.Ctx) error
}
func NewArticleFilesController(articleFilesService service.ArticleFilesService) ArticleFilesController {
return &articleFilesController{
articleFilesService: articleFilesService,
}
}
// All ArticleFiles
// @Summary Get all ArticleFiles
// @Description API for getting all ArticleFiles
// @Tags Article Files
// @Security Bearer
// @Param req query request.ArticleFilesQueryRequest false "query parameters"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-files [get]
func (_i *articleFilesController) All(c *fiber.Ctx) error {
// Get from context
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
reqContext := request.ArticleFilesQueryRequestContext{
ArticleId: c.Query("articleId"),
FileName: c.Query("fileName"),
StatusId: c.Query("statusId"),
IsPublish: c.Query("isPublish"),
}
req := reqContext.ToParamRequest()
req.Pagination = paginate
articleFilesData, paging, err := _i.articleFilesService.All(req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleFiles list successfully retrieved"},
Data: articleFilesData,
Meta: paging,
})
}
// Show ArticleFiles
// @Summary Get one ArticleFiles
// @Description API for getting one ArticleFiles
// @Tags Article Files
// @Security Bearer
// @Param id path int true "ArticleFiles ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-files/{id} [get]
func (_i *articleFilesController) Show(c *fiber.Ctx) error {
// Get from context
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
articleFilesData, err := _i.articleFilesService.Show(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleFiles successfully retrieved"},
Data: articleFilesData,
})
}
// Save ArticleFiles
// @Summary Upload ArticleFiles
// @Description API for create ArticleFiles
// @Tags Article Files
// @Security Bearer
// @Produce json
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param files formData file true "Upload file" multiple true
// @Param articleId path int true "Article ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-files/{articleId} [post]
func (_i *articleFilesController) Save(c *fiber.Ctx) error {
// Get from context
id, err := strconv.ParseUint(c.Params("articleId"), 10, 0)
if err != nil {
return err
}
err = _i.articleFilesService.Save(c, uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleFiles successfully upload"},
})
}
// Update ArticleFiles
// @Summary Update ArticleFiles
// @Description API for update ArticleFiles
// @Tags Article Files
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param payload body request.ArticleFilesUpdateRequest true "Required payload"
// @Param id path int true "ArticleFiles ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-files/{id} [put]
func (_i *articleFilesController) Update(c *fiber.Ctx) error {
// Get from context
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
req := new(request.ArticleFilesUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err = _i.articleFilesService.Update(uint(id), *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleFiles successfully updated"},
})
}
// Delete ArticleFiles
// @Summary Delete ArticleFiles
// @Description API for delete ArticleFiles
// @Tags Article Files
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param id path int true "ArticleFiles ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-files/{id} [delete]
func (_i *articleFilesController) Delete(c *fiber.Ctx) error {
// Get from context
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
err = _i.articleFilesService.Delete(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleFiles successfully deleted"},
})
}
// Viewer ArticleFiles
// @Summary Viewer ArticleFiles
// @Description API for Viewer ArticleFiles
// @Tags Article Files
// @Security Bearer
// @Param filename path string true "Article File Name"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-files/viewer/{filename} [get]
func (_i *articleFilesController) Viewer(c *fiber.Ctx) error {
// Get from context
return _i.articleFilesService.Viewer(c)
}
// GetUploadStatus ArticleFiles
// @Summary GetUploadStatus ArticleFiles
// @Description API for GetUploadStatus ArticleFiles
// @Tags Article Files
// @Security Bearer
// @Param uploadId path string true "Upload ID of ArticleFiles"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-files/upload-status/{uploadId} [get]
func (_i *articleFilesController) GetUploadStatus(c *fiber.Ctx) error {
progress, _ := _i.articleFilesService.GetUploadStatus(c)
progressMessage := fmt.Sprintf("Upload Progress: %d%%", progress)
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Upload Status Retrieve"},
Data: progressMessage,
})
}

View File

@ -0,0 +1,13 @@
package controller
import "campaign-pool-be/app/module/article_files/service"
type Controller struct {
ArticleFiles ArticleFilesController
}
func NewController(ArticleFilesService service.ArticleFilesService) *Controller {
return &Controller{
ArticleFiles: NewArticleFilesController(ArticleFilesService),
}
}

View File

@ -0,0 +1,37 @@
package mapper
import (
"campaign-pool-be/app/database/entity"
res "campaign-pool-be/app/module/article_files/response"
)
func ArticleFilesResponseMapper(articleFilesReq *entity.ArticleFiles, host string) (articleFilesRes *res.ArticleFilesResponse) {
fileUrl := host + "/article-files/viewer/"
if articleFilesReq.FileName != nil {
fileUrl += *articleFilesReq.FileName
}
if articleFilesReq != nil {
articleFilesRes = &res.ArticleFilesResponse{
ID: articleFilesReq.ID,
ArticleId: articleFilesReq.ArticleId,
FilePath: articleFilesReq.FilePath,
FileUrl: &fileUrl,
FileName: articleFilesReq.FileName,
FileThumbnail: articleFilesReq.FileThumbnail,
FileAlt: articleFilesReq.FileAlt,
WidthPixel: articleFilesReq.WidthPixel,
HeightPixel: articleFilesReq.HeightPixel,
Size: articleFilesReq.Size,
DownloadCount: articleFilesReq.DownloadCount,
CreatedById: articleFilesReq.CreatedById,
StatusId: articleFilesReq.StatusId,
IsPublish: articleFilesReq.IsPublish,
PublishedAt: articleFilesReq.PublishedAt,
IsActive: articleFilesReq.IsActive,
CreatedAt: articleFilesReq.CreatedAt,
UpdatedAt: articleFilesReq.UpdatedAt,
}
}
return articleFilesRes
}

View File

@ -0,0 +1,123 @@
package repository
import (
"campaign-pool-be/app/database"
"campaign-pool-be/app/database/entity"
"campaign-pool-be/app/module/article_files/request"
"campaign-pool-be/utils/paginator"
utilSvc "campaign-pool-be/utils/service"
"fmt"
"strings"
)
type articleFilesRepository struct {
DB *database.Database
}
// ArticleFilesRepository define interface of IArticleFilesRepository
type ArticleFilesRepository interface {
GetAll(req request.ArticleFilesQueryRequest) (articleFiless []*entity.ArticleFiles, paging paginator.Pagination, err error)
FindOne(id uint) (articleFiles *entity.ArticleFiles, err error)
FindByArticle(articleId uint) (articleFiles []*entity.ArticleFiles, err error)
FindByFilename(filename string) (articleFiles *entity.ArticleFiles, err error)
Create(articleFiles *entity.ArticleFiles) (err error)
Update(id uint, articleFiles *entity.ArticleFiles) (err error)
Delete(id uint) (err error)
}
func NewArticleFilesRepository(db *database.Database) ArticleFilesRepository {
return &articleFilesRepository{
DB: db,
}
}
// implement interface of IArticleFilesRepository
func (_i *articleFilesRepository) GetAll(req request.ArticleFilesQueryRequest) (articleFiless []*entity.ArticleFiles, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Model(&entity.ArticleFiles{})
query = query.Where("is_active = ?", true)
if req.ArticleId != nil {
query = query.Where("article_id = ?", req.ArticleId)
}
if req.FileName != nil && *req.FileName != "" {
fileName := strings.ToLower(*req.FileName)
query = query.Where("LOWER(file_name) LIKE ?", "%"+strings.ToLower(fileName)+"%")
}
if req.IsPublish != nil {
query = query.Where("is_publish = ?", req.IsPublish)
}
if req.StatusId != nil {
query = query.Where("status_id = ?", req.StatusId)
}
query.Count(&count)
if req.Pagination.SortBy != "" {
direction := "ASC"
if req.Pagination.Sort == "desc" {
direction = "DESC"
}
query.Order(fmt.Sprintf("%s %s", req.Pagination.SortBy, direction))
}
req.Pagination.Count = count
req.Pagination = paginator.Paging(req.Pagination)
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&articleFiless).Error
if err != nil {
return
}
paging = *req.Pagination
return
}
func (_i *articleFilesRepository) FindOne(id uint) (articleFiles *entity.ArticleFiles, err error) {
query := _i.DB.DB
if err := query.First(&articleFiles, id).Error; err != nil {
return nil, err
}
return articleFiles, nil
}
func (_i *articleFilesRepository) FindByArticle(articleId uint) (articleFiles []*entity.ArticleFiles, err error) {
query := _i.DB.DB.Where("article_id = ?", articleId)
if err := query.Find(&articleFiles).Error; err != nil {
return nil, err
}
return articleFiles, nil
}
func (_i *articleFilesRepository) FindByFilename(articleFilename string) (articleFiles *entity.ArticleFiles, err error) {
query := _i.DB.DB.Where("file_name = ?", articleFilename)
if err := query.First(&articleFiles).Error; err != nil {
return nil, err
}
return articleFiles, nil
}
func (_i *articleFilesRepository) Create(articleFiles *entity.ArticleFiles) (err error) {
return _i.DB.DB.Create(articleFiles).Error
}
func (_i *articleFilesRepository) Update(id uint, articleFiles *entity.ArticleFiles) (err error) {
articleFilesMap, err := utilSvc.StructToMap(articleFiles)
if err != nil {
return err
}
return _i.DB.DB.Model(&entity.ArticleFiles{}).
Where(&entity.ArticleFiles{ID: id}).
Updates(articleFilesMap).Error
}
func (_i *articleFilesRepository) Delete(id uint) error {
return _i.DB.DB.Delete(&entity.ArticleFiles{}, id).Error
}

View File

@ -0,0 +1,120 @@
package request
import (
"campaign-pool-be/app/database/entity"
"campaign-pool-be/utils/paginator"
"strconv"
"time"
)
type ArticleFilesGeneric interface {
ToEntity()
}
type ArticleFilesQueryRequest struct {
ArticleId *int `json:"articleId"`
FileName *string `json:"fileName"`
StatusId *int `json:"statusId"`
IsPublish *bool `json:"isPublish"`
Pagination *paginator.Pagination `json:"pagination"`
}
type ArticleFilesCreateRequest struct {
ArticleId uint `json:"articleId" validate:"required"`
StatusId int `json:"statusId" validate:"required"`
UploadId *string `json:"uploadId"`
FilePath *string `json:"filePath"`
FileUrl *string `json:"fileUrl"`
FileName *string `json:"fileName"`
FileThumbnail *string `json:"fileThumbnail"`
FileAlt *string `json:"fileAlt"`
WidthPixel *string `json:"widthPixel"`
HeightPixel *string `json:"heightPixel"`
Size *string `json:"size"`
}
func (req ArticleFilesCreateRequest) ToEntity() *entity.ArticleFiles {
return &entity.ArticleFiles{
ArticleId: req.ArticleId,
UploadID: req.UploadId,
FilePath: req.FilePath,
FileUrl: req.FileUrl,
FileName: req.FileName,
FileThumbnail: req.FileThumbnail,
FileAlt: req.FileAlt,
WidthPixel: req.WidthPixel,
HeightPixel: req.HeightPixel,
Size: req.Size,
StatusId: req.StatusId,
}
}
type ArticleFilesUpdateRequest struct {
ID uint `json:"id" validate:"required"`
ArticleId uint `json:"articleId" validate:"required"`
StatusId int `json:"statusId" validate:"required"`
FilePath *string `json:"filePath"`
FileUrl *string `json:"fileUrl"`
FileName *string `json:"fileName"`
FileThumbnail *string `json:"fileThumbnail"`
FileAlt *string `json:"fileAlt"`
WidthPixel *string `json:"widthPixel"`
HeightPixel *string `json:"heightPixel"`
Size *string `json:"size"`
IsPublish *bool `json:"isPublish" validate:"required"`
PublishedAt *time.Time `json:"publishedAt" validate:"required"`
}
func (req ArticleFilesUpdateRequest) ToEntity() *entity.ArticleFiles {
return &entity.ArticleFiles{
ID: req.ID,
ArticleId: req.ArticleId,
FilePath: req.FilePath,
FileUrl: req.FileUrl,
FileName: req.FileName,
FileThumbnail: req.FileThumbnail,
FileAlt: req.FileAlt,
WidthPixel: req.WidthPixel,
HeightPixel: req.HeightPixel,
Size: req.Size,
StatusId: req.StatusId,
IsPublish: req.IsPublish,
PublishedAt: req.PublishedAt,
UpdatedAt: time.Now(),
}
}
type ArticleFilesQueryRequestContext struct {
ArticleId string `json:"articleId"`
FileName string `json:"fileName"`
StatusId string `json:"statusId"`
IsPublish string `json:"isPublish"`
}
func (req ArticleFilesQueryRequestContext) ToParamRequest() ArticleFilesQueryRequest {
var request ArticleFilesQueryRequest
if articleIdStr := req.ArticleId; articleIdStr != "" {
articleId, err := strconv.Atoi(articleIdStr)
if err == nil {
request.ArticleId = &articleId
}
}
if fileName := req.FileName; fileName != "" {
request.FileName = &fileName
}
if statusIdStr := req.StatusId; statusIdStr != "" {
statusId, err := strconv.Atoi(statusIdStr)
if err == nil {
request.StatusId = &statusId
}
}
if isPublishStr := req.IsPublish; isPublishStr != "" {
isPublish, err := strconv.ParseBool(isPublishStr)
if err == nil {
request.IsPublish = &isPublish
}
}
return request
}

View File

@ -0,0 +1,24 @@
package response
import "time"
type ArticleFilesResponse struct {
ID uint `json:"id"`
ArticleId uint `json:"articleId"`
FilePath *string `json:"filePath"`
FileUrl *string `json:"fileUrl"`
FileName *string `json:"fileName"`
FileThumbnail *string `json:"fileThumbnail"`
FileAlt *string `json:"fileAlt"`
WidthPixel *string `json:"widthPixel"`
HeightPixel *string `json:"heightPixel"`
Size *string `json:"size"`
DownloadCount *int `json:"downloadCount"`
CreatedById int `json:"createdById"`
StatusId int `json:"statusId"`
IsPublish *bool `json:"isPublish"`
PublishedAt *time.Time `json:"publishedAt"`
IsActive bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}

View File

@ -0,0 +1,446 @@
package service
import (
"campaign-pool-be/app/module/article_files/mapper"
"campaign-pool-be/app/module/article_files/repository"
"campaign-pool-be/app/module/article_files/request"
"campaign-pool-be/app/module/article_files/response"
config "campaign-pool-be/config/config"
minioStorage "campaign-pool-be/config/config"
"campaign-pool-be/utils/paginator"
"context"
"fmt"
"io"
"log"
"math/rand"
"mime"
"mime/multipart"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/gofiber/fiber/v2"
"github.com/minio/minio-go/v7"
"github.com/rs/zerolog"
)
// ArticleFilesService
type articleFilesService struct {
Repo repository.ArticleFilesRepository
Log zerolog.Logger
Cfg *config.Config
MinioStorage *minioStorage.MinioStorage
}
// ArticleFilesService define interface of IArticleFilesService
type ArticleFilesService interface {
All(req request.ArticleFilesQueryRequest) (articleFiles []*response.ArticleFilesResponse, paging paginator.Pagination, err error)
Show(id uint) (articleFiles *response.ArticleFilesResponse, err error)
Save(c *fiber.Ctx, id uint) error
SaveAsync(c *fiber.Ctx, id uint) error
Update(id uint, req request.ArticleFilesUpdateRequest) (err error)
GetUploadStatus(c *fiber.Ctx) (progress int, err error)
Delete(id uint) error
Viewer(c *fiber.Ctx) error
}
// NewArticleFilesService init ArticleFilesService
func NewArticleFilesService(repo repository.ArticleFilesRepository, log zerolog.Logger, cfg *config.Config, minioStorage *minioStorage.MinioStorage) ArticleFilesService {
return &articleFilesService{
Repo: repo,
Log: log,
Cfg: cfg,
MinioStorage: minioStorage,
}
}
var (
progressMap = make(map[string]int) // Menyimpan progress upload per UploadID
progressLock = sync.Mutex{}
)
type progressWriter struct {
uploadID string
totalSize int64
uploadedSize *int64
}
// All implement interface of ArticleFilesService
func (_i *articleFilesService) All(req request.ArticleFilesQueryRequest) (articleFiless []*response.ArticleFilesResponse, paging paginator.Pagination, err error) {
results, paging, err := _i.Repo.GetAll(req)
if err != nil {
return
}
host := _i.Cfg.App.Domain
for _, result := range results {
articleFiless = append(articleFiless, mapper.ArticleFilesResponseMapper(result, host))
}
return
}
func (_i *articleFilesService) Show(id uint) (articleFiles *response.ArticleFilesResponse, err error) {
result, err := _i.Repo.FindOne(id)
if err != nil {
return nil, err
}
host := _i.Cfg.App.Domain
return mapper.ArticleFilesResponseMapper(result, host), nil
}
func (_i *articleFilesService) SaveAsync(c *fiber.Ctx, id uint) (err error) {
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
ctx := context.Background()
form, err := c.MultipartForm()
if err != nil {
return err
}
//filess := form.File["files"]
// Create minio connection.
minioClient, err := _i.MinioStorage.ConnectMinio()
if err != nil {
// Return status 500 and minio connection error.
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}
for _, files := range form.File {
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Uploader:: top").
Interface("files", files).Msg("")
for _, fileHeader := range files {
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Uploader:: loop").
Interface("data", fileHeader).Msg("")
filename := filepath.Base(fileHeader.Filename)
filenameAlt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))])
filename = strings.ReplaceAll(filename, " ", "")
filenameWithoutExt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))])
extension := filepath.Ext(fileHeader.Filename)[1:]
now := time.Now()
rand.New(rand.NewSource(now.UnixNano()))
randUniqueId := rand.Intn(1000000)
uploadID := strconv.Itoa(randUniqueId)
newFilenameWithoutExt := filenameWithoutExt + "_" + strconv.Itoa(randUniqueId)
newFilename := newFilenameWithoutExt + "." + extension
objectName := fmt.Sprintf("articles/upload/%d/%d/%s", now.Year(), now.Month(), newFilename)
fileSize := strconv.FormatInt(fileHeader.Size, 10)
fileSizeInt := fileHeader.Size
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Uploader:: top").
Interface("Start upload", uploadID).Msg("")
req := request.ArticleFilesCreateRequest{
ArticleId: id,
UploadId: &uploadID,
FilePath: &objectName,
FileName: &newFilename,
FileAlt: &filenameAlt,
Size: &fileSize,
}
err = _i.Repo.Create(req.ToEntity())
if err != nil {
return err
}
src, err := fileHeader.Open()
if err != nil {
return err
}
defer src.Close()
tempFilePath := fmt.Sprintf("/tmp/%s", newFilename)
tempFile, err := os.Create(tempFilePath)
if err != nil {
return err
}
defer tempFile.Close()
// Copy file ke direktori sementara
_, err = io.Copy(tempFile, src)
if err != nil {
return err
}
go uploadToMinIO(ctx, _i.Log, minioClient, uploadID, tempFilePath, bucketName, objectName, fileSizeInt)
}
}
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "User:All").
Interface("data", "Successfully uploaded").Msg("")
return
}
func (_i *articleFilesService) Save(c *fiber.Ctx, id uint) (err error) {
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
form, err := c.MultipartForm()
if err != nil {
return err
}
//filess := form.File["files"]
// Create minio connection.
minioClient, err := _i.MinioStorage.ConnectMinio()
if err != nil {
// Return status 500 and minio connection error.
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}
for _, files := range form.File {
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Uploader:: top").
Interface("files", files).Msg("")
for _, fileHeader := range files {
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Uploader:: loop").
Interface("data", fileHeader).Msg("")
src, err := fileHeader.Open()
if err != nil {
return err
}
defer src.Close()
filename := filepath.Base(fileHeader.Filename)
filenameAlt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))])
filename = strings.ReplaceAll(filename, " ", "")
filenameWithoutExt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))])
extension := filepath.Ext(fileHeader.Filename)[1:]
now := time.Now()
rand.New(rand.NewSource(now.UnixNano()))
randUniqueId := rand.Intn(1000000)
newFilenameWithoutExt := filenameWithoutExt + "_" + strconv.Itoa(randUniqueId)
newFilename := newFilenameWithoutExt + "." + extension
objectName := fmt.Sprintf("articles/upload/%d/%d/%s", now.Year(), now.Month(), newFilename)
fileSize := strconv.FormatInt(fileHeader.Size, 10)
req := request.ArticleFilesCreateRequest{
ArticleId: id,
FilePath: &objectName,
FileName: &newFilename,
FileAlt: &filenameAlt,
Size: &fileSize,
}
err = _i.Repo.Create(req.ToEntity())
if err != nil {
return err
}
// Upload file ke MinIO
_, err = minioClient.PutObject(context.Background(), bucketName, objectName, src, fileHeader.Size, minio.PutObjectOptions{})
if err != nil {
return err
}
}
}
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "User:All").
Interface("data", "Successfully uploaded").Msg("")
return
}
func (_i *articleFilesService) Update(id uint, req request.ArticleFilesUpdateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("")
return _i.Repo.Update(id, req.ToEntity())
}
func (_i *articleFilesService) Delete(id uint) error {
result, err := _i.Repo.FindOne(id)
if err != nil {
return err
}
result.IsActive = false
return _i.Repo.Update(id, result)
}
func (_i *articleFilesService) Viewer(c *fiber.Ctx) (err error) {
filename := c.Params("filename")
result, err := _i.Repo.FindByFilename(filename)
if err != nil {
return err
}
ctx := context.Background()
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
objectName := *result.FilePath
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Article:Uploads").
Interface("data", objectName).Msg("")
// Create minio connection.
minioClient, err := _i.MinioStorage.ConnectMinio()
if err != nil {
// Return status 500 and minio connection error.
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}
fileContent, err := minioClient.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{})
if err != nil {
log.Fatalln(err)
}
defer fileContent.Close()
// Tentukan Content-Type berdasarkan ekstensi file
contentType := mime.TypeByExtension("." + getFileExtension(objectName))
if contentType == "" {
contentType = "application/octet-stream" // fallback jika tidak ada tipe MIME yang cocok
}
c.Set("Content-Type", contentType)
if _, err := io.Copy(c.Response().BodyWriter(), fileContent); err != nil {
return err
}
return
}
func getFileExtension(filename string) string {
// split file name
parts := strings.Split(filename, ".")
// jika tidak ada ekstensi, kembalikan string kosong
if len(parts) == 1 || (len(parts) == 2 && parts[0] == "") {
return ""
}
// ambil ekstensi terakhir
return parts[len(parts)-1]
}
func uploadTempFile(log zerolog.Logger, fileHeader *multipart.FileHeader, filePath string) {
src, err := fileHeader.Open()
if err != nil {
log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Article:uploadToMinIO-0").
Interface("err", err).Msg("")
}
defer src.Close()
tempFile, err := os.Create(filePath)
if err != nil {
log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Article:uploadToMinIO-1").
Interface("err", err).Msg("")
}
defer tempFile.Close()
// Copy file ke direktori sementara
_, err = io.Copy(tempFile, src)
if err != nil {
log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Article:uploadToMinIO-2").
Interface("err", err).Msg("")
}
}
func uploadToMinIO(ctx context.Context, log zerolog.Logger, minioClient *minio.Client, uploadID, filePath, bucketName string, objectName string, fileSize int64) {
file, err := os.Open(filePath)
if err != nil {
log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Article:uploadToMinIO-3").
Interface("err", err).Msg("")
return
}
defer file.Close()
// Upload file ke MinIO dengan progress tracking
uploadProgress := int64(0)
reader := io.TeeReader(file, &progressWriter{uploadID: uploadID, totalSize: fileSize, uploadedSize: &uploadProgress})
_, err = minioClient.PutObject(ctx, bucketName, objectName, reader, fileSize, minio.PutObjectOptions{})
if err != nil {
log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Article:uploadToMinIO-4").
Interface("err", err).Msg("")
return
}
// Upload selesai, update progress menjadi 100
progressLock.Lock()
progressMap[uploadID] = 100
progressLock.Unlock()
go removeFileTemp(log, filePath)
}
func removeFileTemp(log zerolog.Logger, filePath string) {
err := os.Remove(filePath)
if err != nil {
log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Article:uploadToMinIO-5").
Interface("Failed to remove temporary file", err).Msg("")
} else {
log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Article:uploadToMinIO-6").
Interface("err", "Temporary file removed").Msg("")
}
}
func (p *progressWriter) Write(data []byte) (int, error) {
n := len(data)
progressLock.Lock()
defer progressLock.Unlock()
*p.uploadedSize += int64(n)
progress := int(float64(*p.uploadedSize) / float64(p.totalSize) * 100)
// Update progress di map
progressMap[p.uploadID] = progress
return n, nil
}
func (_i *articleFilesService) GetUploadStatus(c *fiber.Ctx) (progress int, err error) {
uploadID := c.Params("uploadId")
// Ambil progress dari map
progressLock.Lock()
progress, _ = progressMap[uploadID]
progressLock.Unlock()
return progress, nil
}

View File

@ -0,0 +1,139 @@
package service
import (
"context"
"github.com/minio/minio-go/v7"
"github.com/rs/zerolog"
"io"
"time"
)
// AsyncUploader menangani proses upload secara asynchronous
type UploadService interface {
UploadFile(ctx context.Context, minioClient *minio.Client, uploadID string, reader io.Reader, bucketName string, objectName string, size int64, contentType string) error
}
type uploadService struct {
uploadManager UploadManager
Log zerolog.Logger
}
func NewUploadService(uploadManager UploadManager, log zerolog.Logger) UploadService {
return &uploadService{
uploadManager: uploadManager,
Log: log,
}
}
func (u *uploadService) UploadFile(ctx context.Context, minioClient *minio.Client, uploadID string, reader io.Reader, bucketName string, objectName string, size int64, contentType string) error {
status := &UploadStatus{
FileName: objectName,
Size: size,
Progress: 0,
Status: "uploading",
ObjectName: objectName,
BucketName: bucketName,
StartTime: time.Now(),
}
u.uploadManager.Add(uploadID, status)
u.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "UploadService::UploadFile").
Interface("add status", status).Msg("")
// Upload ke Minio
_, err := minioClient.PutObject(
ctx,
bucketName,
objectName,
reader,
size,
minio.PutObjectOptions{
ContentType: contentType,
PartSize: 10 * 1024 * 1024, // 10MB part size
},
)
if err != nil {
u.uploadManager.UpdateStatus(uploadID, "error", err)
u.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "UploadService::UploadFile").
Interface("error when upload", err).Msg("")
}
u.uploadManager.UpdateStatus(uploadID, "completed", nil)
return nil
}
//func (au *UploadService) UploadFile() {
// // Buat context dengan timeout
// ctx, cancel := context.WithTimeout(au.ctx, 30*time.Minute)
// defer cancel()
//
// // Buat reader dari byte slice
// reader := bytes.NewReader(au.fileData)
// pipeReader, pipeWriter := io.Pipe()
//
// au.progressMap.Store(au.uploadID, 0.0)
//
// // Start goroutine to read from reader and write to pipe
// go func() {
// defer pipeWriter.Close()
// buf := make([]byte, au.partSize)
//
// totalParts := int(reader.Size() / au.partSize)
// if reader.Size()%au.partSize != 0 {
// totalParts++
// }
//
// for i := 0; i < totalParts; i++ {
// n, err := reader.Read(buf)
// if err != nil && err != io.EOF {
// log.Println("Error reading file:", err)
// return
// }
//
// if _, err := pipeWriter.Write(buf[:n]); err != nil {
// log.Println("Error writing to pipe:", err)
// return
// }
//
// progress := float64(i+1) / float64(totalParts) * 100
// au.progressMap.Store(au.uploadID, progress)
// au.uploadManager.UpdateProgress(au.uploadID, int(progress))
// }
// }()
//
// // Upload ke Minio
// _, err := au.minioClient.PutObject(
// ctx,
// au.bucketName,
// au.objectName,
// pipeReader,
// reader.Size(),
// minio.PutObjectOptions{
// ContentType: au.contentType,
// PartSize: 10 * 1024 * 1024, // 10MB part size
// },
// )
//
// if err != nil {
// log.Println("Error uploading file:", err)
// au.progressMap.Store(au.uploadID, "error")
// return
// }
//
// fmt.Printf("Uploading process for %s", au.objectName)
//
// if err != nil {
// uploadManager.UpdateStatus(au.uploadID, "error", err)
// fmt.Printf("Upload error for %s: %v\n", au.objectName, err)
// return
// }
//
// au.progressMap.Store(au.uploadID, 100)
// au.uploadManager.UpdateProgress(au.uploadID, 100)
// au.uploadManager.UpdateStatus(au.uploadID, "completed", nil)
//}

View File

@ -0,0 +1,71 @@
package service
import (
"sync"
"time"
)
type UploadStatus struct {
FileName string `json:"fileName"`
Size int64 `json:"size"`
Progress int `json:"progress"`
Status string `json:"status"`
ObjectName string `json:"objectName"`
BucketName string `json:"bucketName"`
StartTime time.Time `json:"startTime"`
Error string `json:"error,omitempty"`
}
type UploadManager interface {
Add(uploadID string, status *UploadStatus)
UpdateProgress(uploadID string, progress int)
UpdateStatus(uploadID string, status string, err error)
Get(uploadID string) (*UploadStatus, bool)
}
type uploadManager struct {
uploads map[string]*UploadStatus
mutex sync.RWMutex
}
func NewUploadManager() UploadManager {
return &uploadManager{
uploads: make(map[string]*UploadStatus),
}
}
// Add menambahkan status upload baru
func (um *uploadManager) Add(uploadID string, status *UploadStatus) {
um.mutex.Lock()
defer um.mutex.Unlock()
um.uploads[uploadID] = status
}
// UpdateProgress memperbarui progress upload
func (um *uploadManager) UpdateProgress(uploadID string, progress int) {
um.mutex.Lock()
defer um.mutex.Unlock()
if status, exists := um.uploads[uploadID]; exists {
status.Progress = progress
}
}
// UpdateStatus memperbarui status upload
func (um *uploadManager) UpdateStatus(uploadID string, status string, err error) {
um.mutex.Lock()
defer um.mutex.Unlock()
if upload, exists := um.uploads[uploadID]; exists {
upload.Status = status
if err != nil {
upload.Error = err.Error()
}
}
}
// Get mendapatkan status upload berdasarkan ID
func (um *uploadManager) Get(uploadID string) (*UploadStatus, bool) {
um.mutex.RLock()
defer um.mutex.RUnlock()
status, exists := um.uploads[uploadID]
return status, exists
}

View File

@ -0,0 +1,62 @@
package articles
import (
"campaign-pool-be/app/module/articles/controller"
"campaign-pool-be/app/module/articles/repository"
"campaign-pool-be/app/module/articles/service"
"github.com/gofiber/fiber/v2"
"go.uber.org/fx"
)
// ArticlesRouter struct of ArticlesRouter
type ArticlesRouter struct {
App fiber.Router
Controller *controller.Controller
}
// NewArticlesModule register bulky of Articles module
var NewArticlesModule = fx.Options(
// register repository of Articles module
fx.Provide(repository.NewArticlesRepository),
// register service of Articles module
fx.Provide(service.NewArticlesService),
// register controller of Articles module
fx.Provide(controller.NewController),
// register router of Articles module
fx.Provide(NewArticlesRouter),
)
// NewArticlesRouter init ArticlesRouter
func NewArticlesRouter(fiber *fiber.App, controller *controller.Controller) *ArticlesRouter {
return &ArticlesRouter{
App: fiber,
Controller: controller,
}
}
// RegisterArticlesRoutes register routes of Articles module
func (_i *ArticlesRouter) RegisterArticlesRoutes() {
// define controllers
articlesController := _i.Controller.Articles
// define routes
_i.App.Route("/articles", func(router fiber.Router) {
router.Get("/", articlesController.All)
router.Get("/old-id/:id", articlesController.ShowByOldId)
router.Get("/:id", articlesController.Show)
router.Post("/", articlesController.Save)
router.Put("/:id", articlesController.Update)
router.Put("/banner/:id", articlesController.UpdateBanner)
router.Post("/thumbnail/:id", articlesController.SaveThumbnail)
router.Get("/thumbnail/viewer/:thumbnailName", articlesController.Viewer)
router.Post("/publish-scheduling", articlesController.PublishScheduling)
router.Delete("/:id", articlesController.Delete)
router.Get("/statistic/summary", articlesController.SummaryStats)
router.Get("/statistic/user-levels", articlesController.ArticlePerUserLevelStats)
router.Get("/statistic/monthly", articlesController.ArticleMonthlyStats)
})
}

View File

@ -0,0 +1,452 @@
package controller
import (
"campaign-pool-be/app/module/articles/request"
"campaign-pool-be/app/module/articles/service"
"campaign-pool-be/utils/paginator"
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog"
utilRes "campaign-pool-be/utils/response"
utilVal "campaign-pool-be/utils/validator"
)
type articlesController struct {
articlesService service.ArticlesService
Log zerolog.Logger
}
type ArticlesController interface {
All(c *fiber.Ctx) error
Show(c *fiber.Ctx) error
ShowByOldId(c *fiber.Ctx) error
Save(c *fiber.Ctx) error
SaveThumbnail(c *fiber.Ctx) error
Update(c *fiber.Ctx) error
UpdateBanner(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error
Viewer(c *fiber.Ctx) error
SummaryStats(c *fiber.Ctx) error
ArticlePerUserLevelStats(c *fiber.Ctx) error
ArticleMonthlyStats(c *fiber.Ctx) error
PublishScheduling(c *fiber.Ctx) error
}
func NewArticlesController(articlesService service.ArticlesService, log zerolog.Logger) ArticlesController {
return &articlesController{
articlesService: articlesService,
Log: log,
}
}
// All Articles
// @Summary Get all Articles
// @Description API for getting all Articles
// @Tags Articles
// @Security Bearer
// @Param X-Client-Key header string true "Insert the X-Client-Key"
// @Param req query request.ArticlesQueryRequest false "query parameters"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /articles [get]
func (_i *articlesController) All(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
reqContext := request.ArticlesQueryRequestContext{
Title: c.Query("title"),
Description: c.Query("description"),
Tags: c.Query("tags"),
Category: c.Query("category"),
CategoryId: c.Query("categoryId"),
TypeId: c.Query("typeId"),
StatusId: c.Query("statusId"),
IsPublish: c.Query("isPublish"),
IsDraft: c.Query("isDraft"),
IsBanner: c.Query("isBanner"),
}
req := reqContext.ToParamRequest()
req.Pagination = paginate
articlesData, paging, err := _i.articlesService.All(req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Articles list successfully retrieved"},
Data: articlesData,
Meta: paging,
})
}
// Show Articles
// @Summary Get one Articles
// @Description API for getting one Articles
// @Tags Articles
// @Security Bearer
// @Param id path int true "Articles ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /articles/{id} [get]
func (_i *articlesController) Show(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
// Get from context
articlesData, err := _i.articlesService.Show(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Articles successfully retrieved"},
Data: articlesData,
})
}
// ShowByOldId Articles
// @Summary Get one Articles
// @Description API for getting one Articles
// @Tags Articles
// @Security Bearer
// @Param id path int true "Articles Old ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /articles/old-id/{id} [get]
func (_i *articlesController) ShowByOldId(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
// Get from context
articlesData, err := _i.articlesService.ShowByOldId(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Articles successfully retrieved"},
Data: articlesData,
})
}
// Save Articles
// @Summary Create Articles
// @Description API for create Articles
// @Tags Articles
// @Security Bearer
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param payload body request.ArticlesCreateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /articles [post]
func (_i *articlesController) Save(c *fiber.Ctx) error {
req := new(request.ArticlesCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
authToken := c.Get("Authorization")
// Get from context
dataResult, err := _i.articlesService.Save(*req, authToken)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Articles successfully created"},
Data: dataResult,
})
}
// SaveThumbnail Articles
// @Summary Save Thumbnail Articles
// @Description API for Save Thumbnail of Articles
// @Tags Articles
// @Security Bearer
// @Produce json
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param files formData file true "Upload thumbnail"
// @Param id path int true "Articles ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /articles/thumbnail/{id} [post]
func (_i *articlesController) SaveThumbnail(c *fiber.Ctx) error {
// Get from context
err := _i.articlesService.SaveThumbnail(c)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Thumbnail of Articles successfully created"},
})
}
// Update Articles
// @Summary Update Articles
// @Description API for update Articles
// @Tags Articles
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param payload body request.ArticlesUpdateRequest true "Required payload"
// @Param id path int true "Articles ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /articles/{id} [put]
func (_i *articlesController) Update(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
req := new(request.ArticlesUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
// Get from context
err = _i.articlesService.Update(uint(id), *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Articles successfully updated"},
})
}
// UpdateBanner Articles
// @Summary Update Banner Articles
// @Description API for Update Banner Articles
// @Tags Articles
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param id path int true "Articles ID"
// @Param isBanner query bool true "Articles Banner Status"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /articles/banner/{id} [put]
func (_i *articlesController) UpdateBanner(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
isBanner, err := strconv.ParseBool(c.Query("isBanner"))
if err != nil {
return err
}
// Get from context
err = _i.articlesService.UpdateBanner(uint(id), isBanner)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Articles successfully banner updated"},
})
}
// Delete Articles
// @Summary Delete Articles
// @Description API for delete Articles
// @Tags Articles
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param id path int true "Articles ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /articles/{id} [delete]
func (_i *articlesController) Delete(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
// Get from context
err = _i.articlesService.Delete(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Articles successfully deleted"},
})
}
// Viewer Articles
// @Summary Viewer Articles Thumbnail
// @Description API for View Thumbnail of Article
// @Tags Articles
// @Security Bearer
// @Param thumbnailName path string true "Articles Thumbnail Name"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /articles/thumbnail/viewer/{thumbnailName} [get]
func (_i *articlesController) Viewer(c *fiber.Ctx) error {
// Get from context
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 false "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")
// Get from context
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,
})
}
// ArticlePerUserLevelStats Articles
// @Summary ArticlePerUserLevelStats Articles
// @Description API for ArticlePerUserLevelStats of Article
// @Tags Articles
// @Security Bearer
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param startDate query string false "start date"
// @Param endDate query string false "start date"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /articles/statistic/user-levels [get]
func (_i *articlesController) ArticlePerUserLevelStats(c *fiber.Ctx) error {
authToken := c.Get("Authorization")
startDate := c.Query("startDate")
endDate := c.Query("endDate")
// Get from context
response, err := _i.articlesService.ArticlePerUserLevelStats(authToken, &startDate, &endDate)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticlePerUserLevelStats of Articles successfully retrieved"},
Data: response,
})
}
// ArticleMonthlyStats Articles
// @Summary ArticleMonthlyStats Articles
// @Description API for ArticleMonthlyStats of Article
// @Tags Articles
// @Security Bearer
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param year query int false "year"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /articles/statistic/monthly [get]
func (_i *articlesController) ArticleMonthlyStats(c *fiber.Ctx) error {
authToken := c.Get("Authorization")
year := c.Query("year")
yearInt, err := strconv.Atoi(year)
if err != nil {
return err
}
// Get from context
response, err := _i.articlesService.ArticleMonthlyStats(authToken, &yearInt)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleMonthlyStats of Articles successfully retrieved"},
Data: response,
})
}
// PublishScheduling Articles
// @Summary PublishScheduling Articles
// @Description API for Publish Schedule of Article
// @Tags Articles
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param id query int false "article id"
// @Param date query string false "publish date"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /articles/publish-scheduling [post]
func (_i *articlesController) PublishScheduling(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Query("id"), 10, 0)
if err != nil {
return err
}
date := c.Query("date")
// Get from context
err = _i.articlesService.PublishScheduling(uint(id), date)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Publish Scheduling of Articles successfully saved"},
})
}

View File

@ -0,0 +1,17 @@
package controller
import (
"campaign-pool-be/app/module/articles/service"
"github.com/rs/zerolog"
)
type Controller struct {
Articles ArticlesController
}
func NewController(ArticlesService service.ArticlesService, log zerolog.Logger) *Controller {
return &Controller{
Articles: NewArticlesController(ArticlesService, log),
}
}

View File

@ -0,0 +1,90 @@
package mapper
import (
"campaign-pool-be/app/database/entity"
articleCategoriesMapper "campaign-pool-be/app/module/article_categories/mapper"
articleCategoriesRepository "campaign-pool-be/app/module/article_categories/repository"
articleCategoriesResponse "campaign-pool-be/app/module/article_categories/response"
articleCategoryDetailsRepository "campaign-pool-be/app/module/article_category_details/repository"
articleFilesMapper "campaign-pool-be/app/module/article_files/mapper"
articleFilesRepository "campaign-pool-be/app/module/article_files/repository"
articleFilesResponse "campaign-pool-be/app/module/article_files/response"
res "campaign-pool-be/app/module/articles/response"
usersRepository "campaign-pool-be/app/module/users/repository"
"github.com/rs/zerolog"
)
func ArticlesResponseMapper(
log zerolog.Logger,
host string,
articlesReq *entity.Articles,
articleCategoriesRepo articleCategoriesRepository.ArticleCategoriesRepository,
articleCategoryDetailsRepo articleCategoryDetailsRepository.ArticleCategoryDetailsRepository,
articleFilesRepo articleFilesRepository.ArticleFilesRepository,
usersRepo usersRepository.UsersRepository,
) (articlesRes *res.ArticlesResponse) {
createdByName := ""
if articlesReq.CreatedById != nil {
findUser, _ := usersRepo.FindOne(*articlesReq.CreatedById)
if findUser != nil {
createdByName = findUser.Fullname
}
}
categoryName := ""
articleCategories, _ := articleCategoryDetailsRepo.FindByArticleId(articlesReq.ID)
var articleCategoriesArr []*articleCategoriesResponse.ArticleCategoriesResponse
if articleCategories != nil && len(articleCategories) > 0 {
for _, result := range articleCategories {
articleCategoriesArr = append(articleCategoriesArr, articleCategoriesMapper.ArticleCategoriesResponseMapper(result.Category, host))
}
log.Info().Interface("articleCategoriesArr", articleCategoriesArr).Msg("")
}
articleFiles, _ := articleFilesRepo.FindByArticle(articlesReq.ID)
var articleFilesArr []*articleFilesResponse.ArticleFilesResponse
if articleFiles != nil && len(articleFiles) > 0 {
for _, result := range articleFiles {
articleFilesArr = append(articleFilesArr, articleFilesMapper.ArticleFilesResponseMapper(result, host))
}
}
if articlesReq != nil {
articlesRes = &res.ArticlesResponse{
ID: articlesReq.ID,
Title: articlesReq.Title,
Slug: articlesReq.Slug,
Description: articlesReq.Description,
HtmlDescription: articlesReq.HtmlDescription,
TypeId: articlesReq.TypeId,
Tags: articlesReq.Tags,
CategoryId: articlesReq.CategoryId,
AiArticleId: articlesReq.AiArticleId,
CategoryName: categoryName,
PageUrl: articlesReq.PageUrl,
CreatedById: articlesReq.CreatedById,
CreatedByName: &createdByName,
ShareCount: articlesReq.ShareCount,
ViewCount: articlesReq.ViewCount,
CommentCount: articlesReq.CommentCount,
OldId: articlesReq.OldId,
StatusId: articlesReq.StatusId,
IsBanner: articlesReq.IsBanner,
IsPublish: articlesReq.IsPublish,
PublishedAt: articlesReq.PublishedAt,
IsActive: articlesReq.IsActive,
CreatedAt: articlesReq.CreatedAt,
UpdatedAt: articlesReq.UpdatedAt,
ArticleFiles: articleFilesArr,
ArticleCategories: articleCategoriesArr,
}
if articlesReq.ThumbnailName != nil {
articlesRes.ThumbnailUrl = host + "/articles/thumbnail/viewer/" + *articlesReq.ThumbnailName
}
}
return articlesRes
}

View File

@ -0,0 +1,330 @@
package repository
import (
"campaign-pool-be/app/database"
"campaign-pool-be/app/database/entity"
"campaign-pool-be/app/module/articles/request"
"campaign-pool-be/app/module/articles/response"
"campaign-pool-be/utils/paginator"
utilSvc "campaign-pool-be/utils/service"
"fmt"
"strings"
"time"
"github.com/rs/zerolog"
)
type articlesRepository struct {
DB *database.Database
Log zerolog.Logger
}
// ArticlesRepository define interface of IArticlesRepository
type ArticlesRepository interface {
GetAll(req request.ArticlesQueryRequest) (articless []*entity.Articles, paging paginator.Pagination, err error)
GetAllPublishSchedule() (articless []*entity.Articles, err error)
FindOne(id uint) (articles *entity.Articles, err error)
FindByFilename(thumbnailName string) (articleReturn *entity.Articles, err error)
FindByOldId(oldId uint) (articles *entity.Articles, err error)
Create(articles *entity.Articles) (articleReturn *entity.Articles, err error)
Update(id uint, articles *entity.Articles) (err error)
UpdateSkipNull(id uint, articles *entity.Articles) (err error)
Delete(id uint) (err error)
SummaryStats(userID uint) (articleSummaryStats *response.ArticleSummaryStats, err error)
ArticlePerUserLevelStats(userLevelId *uint, levelNumber *int, startDate *time.Time, endDate *time.Time) (articlePerUserLevelStats []*response.ArticlePerUserLevelStats, err error)
ArticleMonthlyStats(userLevelId *uint, levelNumber *int, year int) (articleMontlyStats []*response.ArticleMonthlyStats, err error)
}
func NewArticlesRepository(db *database.Database, log zerolog.Logger) ArticlesRepository {
return &articlesRepository{
DB: db,
Log: log,
}
}
// implement interface of IArticlesRepository
func (_i *articlesRepository) GetAll(req request.ArticlesQueryRequest) (articless []*entity.Articles, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Model(&entity.Articles{})
if req.CategoryId != nil {
query = query.Joins("JOIN article_category_details acd ON acd.article_id = articles.id").
Where("acd.category_id = ?", req.CategoryId)
}
query = query.Where("articles.is_active = ?", true)
if req.Title != nil && *req.Title != "" {
title := strings.ToLower(*req.Title)
query = query.Where("LOWER(articles.title) LIKE ?", "%"+strings.ToLower(title)+"%")
}
if req.Description != nil && *req.Description != "" {
description := strings.ToLower(*req.Description)
query = query.Where("LOWER(articles.description) LIKE ?", "%"+strings.ToLower(description)+"%")
}
if req.Tags != nil && *req.Tags != "" {
tags := strings.ToLower(*req.Tags)
query = query.Where("LOWER(articles.tags) LIKE ?", "%"+strings.ToLower(tags)+"%")
}
if req.TypeId != nil {
query = query.Where("articles.type_id = ?", req.TypeId)
}
if req.IsPublish != nil {
query = query.Where("articles.is_publish = ?", req.IsPublish)
}
if req.IsBanner != nil {
query = query.Where("articles.is_banner = ?", req.IsBanner)
}
if req.IsDraft != nil {
query = query.Where("articles.is_draft = ?", req.IsDraft)
}
if req.StatusId != nil {
query = query.Where("articles.status_id = ?", req.StatusId)
}
if req.CreatedById != nil {
query = query.Where("articles.created_by_id = ?", req.CreatedById)
}
query.Count(&count)
if req.Pagination.SortBy != "" {
direction := "ASC"
if req.Pagination.Sort == "desc" {
direction = "DESC"
}
query.Order(fmt.Sprintf("%s %s", req.Pagination.SortBy, direction))
} else {
direction := "DESC"
sortBy := "articles.created_at"
query.Order(fmt.Sprintf("%s %s", sortBy, direction))
}
req.Pagination.Count = count
req.Pagination = paginator.Paging(req.Pagination)
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&articless).Error
if err != nil {
return
}
paging = *req.Pagination
return
}
func (_i *articlesRepository) GetAllPublishSchedule() (articles []*entity.Articles, err error) {
query := _i.DB.DB.Where("publish_schedule IS NOT NULL")
err = query.Find(&articles).Error
if err != nil {
return nil, err
}
return articles, nil
}
func (_i *articlesRepository) FindOne(id uint) (articles *entity.Articles, err error) {
query := _i.DB.DB
if err := query.First(&articles, id).Error; err != nil {
return nil, err
}
return articles, nil
}
func (_i *articlesRepository) FindByFilename(thumbnailName string) (articles *entity.Articles, err error) {
query := _i.DB.DB.Where("thumbnail_name = ?", thumbnailName)
if err := query.First(&articles).Error; err != nil {
return nil, err
}
return articles, nil
}
func (_i *articlesRepository) FindByOldId(oldId uint) (articles *entity.Articles, err error) {
query := _i.DB.DB.Where("old_id = ?", oldId)
if err := query.First(&articles).Error; err != nil {
return nil, err
}
return articles, nil
}
func (_i *articlesRepository) Create(articles *entity.Articles) (articleReturn *entity.Articles, err error) {
result := _i.DB.DB.Create(articles)
return articles, result.Error
}
func (_i *articlesRepository) Update(id uint, articles *entity.Articles) (err error) {
articlesMap, err := utilSvc.StructToMap(articles)
if err != nil {
return err
}
return _i.DB.DB.Model(&entity.Articles{}).
Where(&entity.Articles{ID: id}).
Updates(articlesMap).Error
}
func (_i *articlesRepository) UpdateSkipNull(id uint, articles *entity.Articles) (err error) {
return _i.DB.DB.Model(&entity.Articles{}).
Where(&entity.Articles{ID: id}).
Updates(articles).Error
}
func (_i *articlesRepository) Delete(id uint) 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
query := _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)
err = query.Scan(&articleSummaryStats).Error
return articleSummaryStats, err
}
func (_i *articlesRepository) ArticlePerUserLevelStats(userLevelId *uint, levelNumber *int, startDate *time.Time, endDate *time.Time) (articlePerUserLevelStats []*response.ArticlePerUserLevelStats, err error) {
levelNumberTop := 1
query := _i.DB.DB.Model(&entity.Articles{}).
Select("user_levels.id as user_level_id", "user_levels.name as user_level_name", "COUNT(articles.id) as total_article").
Joins("LEFT JOIN users ON articles.created_by_id = users.id").
Joins("LEFT JOIN user_levels ON users.user_level_id = user_levels.id").
Where("articles.is_active = true")
if userLevelId != nil && *levelNumber != levelNumberTop {
query = query.Where("user_levels.id = ? or user_levels.parent_level_id = ?", *userLevelId, *userLevelId)
} else {
query = _i.DB.DB.Raw(`
WITH LevelHierarchy AS (
SELECT
id,
name,
level_number,
parent_level_id,
CASE
WHEN level_number = 1 THEN id
WHEN level_number = 2 and name ILIKE '%polda%' THEN id
WHEN level_number = 2 and name NOT ILIKE '%polda%' THEN parent_level_id
WHEN level_number = 3 THEN parent_level_id
END AS level_2_id,
CASE
WHEN level_number = 1 THEN name
WHEN level_number = 2 and name ILIKE '%polda%' THEN name
WHEN level_number = 2 and name NOT ILIKE '%polda%' THEN (SELECT name FROM user_levels ul2 WHERE ul2.id = user_levels.parent_level_id)
WHEN level_number = 3 THEN (SELECT name FROM user_levels ul2 WHERE ul2.id = user_levels.parent_level_id)
END AS level_2_name
FROM user_levels
)
SELECT
lh.level_2_id AS user_level_id,
UPPER(lh.level_2_name) AS user_level_name,
COUNT(articles.id) AS total_article
FROM articles
JOIN users ON articles.created_by_id = users.id
JOIN LevelHierarchy lh ON users.user_level_id = lh.id
WHERE articles.is_active = true AND lh.level_2_id > 0`)
query = query.Group("lh.level_2_id, lh.level_2_name").Order("total_article DESC")
}
// Apply date filters if provided
if startDate != nil {
query = query.Where("articles.created_at >= ?", *startDate)
}
if endDate != nil {
query = query.Where("articles.created_at <= ?", *endDate)
}
// Group by all non-aggregated columns
err = query.Group("user_levels.id, user_levels.name").
Order("total_article DESC").
Scan(&articlePerUserLevelStats).Error
return articlePerUserLevelStats, err
}
func (_i *articlesRepository) ArticleMonthlyStats(userLevelId *uint, levelNumber *int, year int) (articleMontlyStats []*response.ArticleMonthlyStats, err error) {
levelNumberTop := 1
if year < 1900 || year > 2100 {
return nil, fmt.Errorf("invalid year")
}
var results []struct {
Month int
Day int
TotalView int
TotalComment int
TotalShare int
}
query := _i.DB.DB.Model(&entity.Articles{}).
Select("EXTRACT(MONTH FROM created_at) as month, EXTRACT(DAY FROM created_at) as day, "+
"SUM(view_count) as total_view, "+
"SUM(comment_count) as total_comment, "+
"SUM(share_count) as total_share").
Where("EXTRACT(YEAR FROM created_at) = ?", year)
if userLevelId != nil && *levelNumber != levelNumberTop {
query = _i.DB.DB.Model(&entity.Articles{}).
Select("EXTRACT(MONTH FROM articles.created_at) as month, EXTRACT(DAY FROM articles.created_at) as day, "+
"SUM(articles.view_count) as total_view, "+
"SUM(articles.comment_count) as total_comment, "+
"SUM(articles.share_count) as total_share").
Joins("LEFT JOIN users ON articles.created_by_id = users.id").
Joins("LEFT JOIN user_levels ON users.user_level_id = user_levels.id").
Where("articles.is_active = true").
Where("EXTRACT(YEAR FROM articles.created_at) = ?", year).
Where("(user_levels.id = ? OR user_levels.parent_level_id = ?)", *userLevelId, *userLevelId)
}
err = query.Group("month, day").Scan(&results).Error
if err != nil {
return nil, err
}
// Siapkan struktur untuk menyimpan data bulanan
monthlyAnalytics := make([]*response.ArticleMonthlyStats, 12)
for i := 0; i < 12; i++ {
daysInMonth := time.Date(year, time.Month(i+1), 0, 0, 0, 0, 0, time.UTC).Day()
monthlyAnalytics[i] = &response.ArticleMonthlyStats{
Year: year,
Month: i + 1,
View: make([]int, daysInMonth),
Comment: make([]int, daysInMonth),
Share: make([]int, daysInMonth),
}
}
// Isi data dari hasil agregasi
for _, result := range results {
monthIndex := result.Month - 1
dayIndex := result.Day - 1
if monthIndex >= 0 && monthIndex < 12 {
if dayIndex >= 0 && dayIndex < len(monthlyAnalytics[monthIndex].View) {
monthlyAnalytics[monthIndex].View[dayIndex] = result.TotalView
monthlyAnalytics[monthIndex].Comment[dayIndex] = result.TotalComment
monthlyAnalytics[monthIndex].Share[dayIndex] = result.TotalShare
}
}
}
return monthlyAnalytics, nil
}

Some files were not shown because too many files have changed in this diff Show More