| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 |
- package admin
- import (
- "strconv"
- "strings"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/handler/dto"
- "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
- "github.com/Wei-Shaw/sub2api/internal/pkg/response"
- middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
- "github.com/Wei-Shaw/sub2api/internal/service"
- "github.com/gin-gonic/gin"
- )
- // AnnouncementHandler handles admin announcement management
- type AnnouncementHandler struct {
- announcementService *service.AnnouncementService
- }
- // NewAnnouncementHandler creates a new admin announcement handler
- func NewAnnouncementHandler(announcementService *service.AnnouncementService) *AnnouncementHandler {
- return &AnnouncementHandler{
- announcementService: announcementService,
- }
- }
- type CreateAnnouncementRequest struct {
- Title string `json:"title" binding:"required"`
- Content string `json:"content" binding:"required"`
- Status string `json:"status" binding:"omitempty,oneof=draft active archived"`
- NotifyMode string `json:"notify_mode" binding:"omitempty,oneof=silent popup"`
- Targeting service.AnnouncementTargeting `json:"targeting"`
- StartsAt *int64 `json:"starts_at"` // Unix seconds, 0/empty = immediate
- EndsAt *int64 `json:"ends_at"` // Unix seconds, 0/empty = never
- }
- type UpdateAnnouncementRequest struct {
- Title *string `json:"title"`
- Content *string `json:"content"`
- Status *string `json:"status" binding:"omitempty,oneof=draft active archived"`
- NotifyMode *string `json:"notify_mode" binding:"omitempty,oneof=silent popup"`
- Targeting *service.AnnouncementTargeting `json:"targeting"`
- StartsAt *int64 `json:"starts_at"` // Unix seconds, 0 = clear
- EndsAt *int64 `json:"ends_at"` // Unix seconds, 0 = clear
- }
- // List handles listing announcements with filters
- // GET /api/v1/admin/announcements
- func (h *AnnouncementHandler) List(c *gin.Context) {
- page, pageSize := response.ParsePagination(c)
- status := strings.TrimSpace(c.Query("status"))
- search := strings.TrimSpace(c.Query("search"))
- sortBy := c.DefaultQuery("sort_by", "created_at")
- sortOrder := c.DefaultQuery("sort_order", "desc")
- if len(search) > 200 {
- search = search[:200]
- }
- params := pagination.PaginationParams{
- Page: page,
- PageSize: pageSize,
- SortBy: sortBy,
- SortOrder: sortOrder,
- }
- items, paginationResult, err := h.announcementService.List(
- c.Request.Context(),
- params,
- service.AnnouncementListFilters{Status: status, Search: search},
- )
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- out := make([]dto.Announcement, 0, len(items))
- for i := range items {
- out = append(out, *dto.AnnouncementFromService(&items[i]))
- }
- response.Paginated(c, out, paginationResult.Total, page, pageSize)
- }
- // GetByID handles getting an announcement by ID
- // GET /api/v1/admin/announcements/:id
- func (h *AnnouncementHandler) GetByID(c *gin.Context) {
- announcementID, err := strconv.ParseInt(c.Param("id"), 10, 64)
- if err != nil || announcementID <= 0 {
- response.BadRequest(c, "Invalid announcement ID")
- return
- }
- item, err := h.announcementService.GetByID(c.Request.Context(), announcementID)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, dto.AnnouncementFromService(item))
- }
- // Create handles creating a new announcement
- // POST /api/v1/admin/announcements
- func (h *AnnouncementHandler) Create(c *gin.Context) {
- var req CreateAnnouncementRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not found in context")
- return
- }
- input := &service.CreateAnnouncementInput{
- Title: req.Title,
- Content: req.Content,
- Status: req.Status,
- NotifyMode: req.NotifyMode,
- Targeting: req.Targeting,
- ActorID: &subject.UserID,
- }
- if req.StartsAt != nil && *req.StartsAt > 0 {
- t := time.Unix(*req.StartsAt, 0)
- input.StartsAt = &t
- }
- if req.EndsAt != nil && *req.EndsAt > 0 {
- t := time.Unix(*req.EndsAt, 0)
- input.EndsAt = &t
- }
- created, err := h.announcementService.Create(c.Request.Context(), input)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, dto.AnnouncementFromService(created))
- }
- // Update handles updating an announcement
- // PUT /api/v1/admin/announcements/:id
- func (h *AnnouncementHandler) Update(c *gin.Context) {
- announcementID, err := strconv.ParseInt(c.Param("id"), 10, 64)
- if err != nil || announcementID <= 0 {
- response.BadRequest(c, "Invalid announcement ID")
- return
- }
- var req UpdateAnnouncementRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not found in context")
- return
- }
- input := &service.UpdateAnnouncementInput{
- Title: req.Title,
- Content: req.Content,
- Status: req.Status,
- NotifyMode: req.NotifyMode,
- Targeting: req.Targeting,
- ActorID: &subject.UserID,
- }
- if req.StartsAt != nil {
- if *req.StartsAt == 0 {
- var cleared *time.Time = nil
- input.StartsAt = &cleared
- } else {
- t := time.Unix(*req.StartsAt, 0)
- ptr := &t
- input.StartsAt = &ptr
- }
- }
- if req.EndsAt != nil {
- if *req.EndsAt == 0 {
- var cleared *time.Time = nil
- input.EndsAt = &cleared
- } else {
- t := time.Unix(*req.EndsAt, 0)
- ptr := &t
- input.EndsAt = &ptr
- }
- }
- updated, err := h.announcementService.Update(c.Request.Context(), announcementID, input)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, dto.AnnouncementFromService(updated))
- }
- // Delete handles deleting an announcement
- // DELETE /api/v1/admin/announcements/:id
- func (h *AnnouncementHandler) Delete(c *gin.Context) {
- announcementID, err := strconv.ParseInt(c.Param("id"), 10, 64)
- if err != nil || announcementID <= 0 {
- response.BadRequest(c, "Invalid announcement ID")
- return
- }
- if err := h.announcementService.Delete(c.Request.Context(), announcementID); err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, gin.H{"message": "Announcement deleted successfully"})
- }
- // ListReadStatus handles listing users read status for an announcement
- // GET /api/v1/admin/announcements/:id/read-status
- func (h *AnnouncementHandler) ListReadStatus(c *gin.Context) {
- announcementID, err := strconv.ParseInt(c.Param("id"), 10, 64)
- if err != nil || announcementID <= 0 {
- response.BadRequest(c, "Invalid announcement ID")
- return
- }
- page, pageSize := response.ParsePagination(c)
- params := pagination.PaginationParams{
- Page: page,
- PageSize: pageSize,
- SortBy: c.DefaultQuery("sort_by", "email"),
- SortOrder: c.DefaultQuery("sort_order", "asc"),
- }
- search := strings.TrimSpace(c.Query("search"))
- if len(search) > 200 {
- search = search[:200]
- }
- items, paginationResult, err := h.announcementService.ListUserReadStatus(
- c.Request.Context(),
- announcementID,
- params,
- search,
- )
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Paginated(c, items, paginationResult.Total, page, pageSize)
- }
|