announcement_handler.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. package admin
  2. import (
  3. "strconv"
  4. "strings"
  5. "time"
  6. "github.com/Wei-Shaw/sub2api/internal/handler/dto"
  7. "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
  8. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  9. middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  10. "github.com/Wei-Shaw/sub2api/internal/service"
  11. "github.com/gin-gonic/gin"
  12. )
  13. // AnnouncementHandler handles admin announcement management
  14. type AnnouncementHandler struct {
  15. announcementService *service.AnnouncementService
  16. }
  17. // NewAnnouncementHandler creates a new admin announcement handler
  18. func NewAnnouncementHandler(announcementService *service.AnnouncementService) *AnnouncementHandler {
  19. return &AnnouncementHandler{
  20. announcementService: announcementService,
  21. }
  22. }
  23. type CreateAnnouncementRequest struct {
  24. Title string `json:"title" binding:"required"`
  25. Content string `json:"content" binding:"required"`
  26. Status string `json:"status" binding:"omitempty,oneof=draft active archived"`
  27. NotifyMode string `json:"notify_mode" binding:"omitempty,oneof=silent popup"`
  28. Targeting service.AnnouncementTargeting `json:"targeting"`
  29. StartsAt *int64 `json:"starts_at"` // Unix seconds, 0/empty = immediate
  30. EndsAt *int64 `json:"ends_at"` // Unix seconds, 0/empty = never
  31. }
  32. type UpdateAnnouncementRequest struct {
  33. Title *string `json:"title"`
  34. Content *string `json:"content"`
  35. Status *string `json:"status" binding:"omitempty,oneof=draft active archived"`
  36. NotifyMode *string `json:"notify_mode" binding:"omitempty,oneof=silent popup"`
  37. Targeting *service.AnnouncementTargeting `json:"targeting"`
  38. StartsAt *int64 `json:"starts_at"` // Unix seconds, 0 = clear
  39. EndsAt *int64 `json:"ends_at"` // Unix seconds, 0 = clear
  40. }
  41. // List handles listing announcements with filters
  42. // GET /api/v1/admin/announcements
  43. func (h *AnnouncementHandler) List(c *gin.Context) {
  44. page, pageSize := response.ParsePagination(c)
  45. status := strings.TrimSpace(c.Query("status"))
  46. search := strings.TrimSpace(c.Query("search"))
  47. sortBy := c.DefaultQuery("sort_by", "created_at")
  48. sortOrder := c.DefaultQuery("sort_order", "desc")
  49. if len(search) > 200 {
  50. search = search[:200]
  51. }
  52. params := pagination.PaginationParams{
  53. Page: page,
  54. PageSize: pageSize,
  55. SortBy: sortBy,
  56. SortOrder: sortOrder,
  57. }
  58. items, paginationResult, err := h.announcementService.List(
  59. c.Request.Context(),
  60. params,
  61. service.AnnouncementListFilters{Status: status, Search: search},
  62. )
  63. if err != nil {
  64. response.ErrorFrom(c, err)
  65. return
  66. }
  67. out := make([]dto.Announcement, 0, len(items))
  68. for i := range items {
  69. out = append(out, *dto.AnnouncementFromService(&items[i]))
  70. }
  71. response.Paginated(c, out, paginationResult.Total, page, pageSize)
  72. }
  73. // GetByID handles getting an announcement by ID
  74. // GET /api/v1/admin/announcements/:id
  75. func (h *AnnouncementHandler) GetByID(c *gin.Context) {
  76. announcementID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  77. if err != nil || announcementID <= 0 {
  78. response.BadRequest(c, "Invalid announcement ID")
  79. return
  80. }
  81. item, err := h.announcementService.GetByID(c.Request.Context(), announcementID)
  82. if err != nil {
  83. response.ErrorFrom(c, err)
  84. return
  85. }
  86. response.Success(c, dto.AnnouncementFromService(item))
  87. }
  88. // Create handles creating a new announcement
  89. // POST /api/v1/admin/announcements
  90. func (h *AnnouncementHandler) Create(c *gin.Context) {
  91. var req CreateAnnouncementRequest
  92. if err := c.ShouldBindJSON(&req); err != nil {
  93. response.BadRequest(c, "Invalid request: "+err.Error())
  94. return
  95. }
  96. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  97. if !ok {
  98. response.Unauthorized(c, "User not found in context")
  99. return
  100. }
  101. input := &service.CreateAnnouncementInput{
  102. Title: req.Title,
  103. Content: req.Content,
  104. Status: req.Status,
  105. NotifyMode: req.NotifyMode,
  106. Targeting: req.Targeting,
  107. ActorID: &subject.UserID,
  108. }
  109. if req.StartsAt != nil && *req.StartsAt > 0 {
  110. t := time.Unix(*req.StartsAt, 0)
  111. input.StartsAt = &t
  112. }
  113. if req.EndsAt != nil && *req.EndsAt > 0 {
  114. t := time.Unix(*req.EndsAt, 0)
  115. input.EndsAt = &t
  116. }
  117. created, err := h.announcementService.Create(c.Request.Context(), input)
  118. if err != nil {
  119. response.ErrorFrom(c, err)
  120. return
  121. }
  122. response.Success(c, dto.AnnouncementFromService(created))
  123. }
  124. // Update handles updating an announcement
  125. // PUT /api/v1/admin/announcements/:id
  126. func (h *AnnouncementHandler) Update(c *gin.Context) {
  127. announcementID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  128. if err != nil || announcementID <= 0 {
  129. response.BadRequest(c, "Invalid announcement ID")
  130. return
  131. }
  132. var req UpdateAnnouncementRequest
  133. if err := c.ShouldBindJSON(&req); err != nil {
  134. response.BadRequest(c, "Invalid request: "+err.Error())
  135. return
  136. }
  137. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  138. if !ok {
  139. response.Unauthorized(c, "User not found in context")
  140. return
  141. }
  142. input := &service.UpdateAnnouncementInput{
  143. Title: req.Title,
  144. Content: req.Content,
  145. Status: req.Status,
  146. NotifyMode: req.NotifyMode,
  147. Targeting: req.Targeting,
  148. ActorID: &subject.UserID,
  149. }
  150. if req.StartsAt != nil {
  151. if *req.StartsAt == 0 {
  152. var cleared *time.Time = nil
  153. input.StartsAt = &cleared
  154. } else {
  155. t := time.Unix(*req.StartsAt, 0)
  156. ptr := &t
  157. input.StartsAt = &ptr
  158. }
  159. }
  160. if req.EndsAt != nil {
  161. if *req.EndsAt == 0 {
  162. var cleared *time.Time = nil
  163. input.EndsAt = &cleared
  164. } else {
  165. t := time.Unix(*req.EndsAt, 0)
  166. ptr := &t
  167. input.EndsAt = &ptr
  168. }
  169. }
  170. updated, err := h.announcementService.Update(c.Request.Context(), announcementID, input)
  171. if err != nil {
  172. response.ErrorFrom(c, err)
  173. return
  174. }
  175. response.Success(c, dto.AnnouncementFromService(updated))
  176. }
  177. // Delete handles deleting an announcement
  178. // DELETE /api/v1/admin/announcements/:id
  179. func (h *AnnouncementHandler) Delete(c *gin.Context) {
  180. announcementID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  181. if err != nil || announcementID <= 0 {
  182. response.BadRequest(c, "Invalid announcement ID")
  183. return
  184. }
  185. if err := h.announcementService.Delete(c.Request.Context(), announcementID); err != nil {
  186. response.ErrorFrom(c, err)
  187. return
  188. }
  189. response.Success(c, gin.H{"message": "Announcement deleted successfully"})
  190. }
  191. // ListReadStatus handles listing users read status for an announcement
  192. // GET /api/v1/admin/announcements/:id/read-status
  193. func (h *AnnouncementHandler) ListReadStatus(c *gin.Context) {
  194. announcementID, err := strconv.ParseInt(c.Param("id"), 10, 64)
  195. if err != nil || announcementID <= 0 {
  196. response.BadRequest(c, "Invalid announcement ID")
  197. return
  198. }
  199. page, pageSize := response.ParsePagination(c)
  200. params := pagination.PaginationParams{
  201. Page: page,
  202. PageSize: pageSize,
  203. SortBy: c.DefaultQuery("sort_by", "email"),
  204. SortOrder: c.DefaultQuery("sort_order", "asc"),
  205. }
  206. search := strings.TrimSpace(c.Query("search"))
  207. if len(search) > 200 {
  208. search = search[:200]
  209. }
  210. items, paginationResult, err := h.announcementService.ListUserReadStatus(
  211. c.Request.Context(),
  212. announcementID,
  213. params,
  214. search,
  215. )
  216. if err != nil {
  217. response.ErrorFrom(c, err)
  218. return
  219. }
  220. response.Paginated(c, items, paginationResult.Total, page, pageSize)
  221. }