ticket_handler.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. package admin
  2. import (
  3. "errors"
  4. "strconv"
  5. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  6. middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  7. "github.com/Wei-Shaw/sub2api/internal/service"
  8. "github.com/gin-gonic/gin"
  9. )
  10. // TicketHandler 工单:用户端(JWT 上下文取用户)与管理端共用同一 handler。
  11. type TicketHandler struct {
  12. service *service.TicketService
  13. }
  14. func NewTicketHandler(svc *service.TicketService) *TicketHandler {
  15. return &TicketHandler{service: svc}
  16. }
  17. func (h *TicketHandler) requireService(c *gin.Context) bool {
  18. if h == nil || h.service == nil {
  19. response.ErrorFrom(c, errors.New("ticket service unavailable"))
  20. return false
  21. }
  22. return true
  23. }
  24. func ticketUserID(c *gin.Context) (int64, bool) {
  25. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  26. if !ok || subject.UserID <= 0 {
  27. response.Unauthorized(c, "Unauthorized")
  28. return 0, false
  29. }
  30. return subject.UserID, true
  31. }
  32. func ticketID(c *gin.Context) (int64, bool) {
  33. id, err := strconv.ParseInt(c.Param("id"), 10, 64)
  34. if err != nil || id <= 0 {
  35. response.BadRequest(c, "Invalid ticket ID")
  36. return 0, false
  37. }
  38. return id, true
  39. }
  40. func ticketErr(c *gin.Context, err error) {
  41. switch {
  42. case errors.Is(err, service.ErrTicketNotFound):
  43. response.NotFound(c, "Ticket not found")
  44. case errors.Is(err, service.ErrTicketClosed):
  45. response.BadRequest(c, "Ticket is closed")
  46. default:
  47. response.ErrorFrom(c, err)
  48. }
  49. }
  50. // ListMine 用户工单列表。
  51. // GET /tickets
  52. func (h *TicketHandler) ListMine(c *gin.Context) {
  53. if !h.requireService(c) {
  54. return
  55. }
  56. userID, ok := ticketUserID(c)
  57. if !ok {
  58. return
  59. }
  60. items, err := h.service.ListMine(c.Request.Context(), userID)
  61. if err != nil {
  62. response.ErrorFrom(c, err)
  63. return
  64. }
  65. if items == nil {
  66. items = []service.SupportTicket{}
  67. }
  68. response.Success(c, gin.H{"items": items, "count": len(items)})
  69. }
  70. type ticketCreateRequest struct {
  71. Subject string `json:"subject"`
  72. Body string `json:"body"`
  73. }
  74. // Create 用户建单。
  75. // POST /tickets
  76. func (h *TicketHandler) Create(c *gin.Context) {
  77. if !h.requireService(c) {
  78. return
  79. }
  80. userID, ok := ticketUserID(c)
  81. if !ok {
  82. return
  83. }
  84. var req ticketCreateRequest
  85. if err := c.ShouldBindJSON(&req); err != nil {
  86. response.BadRequest(c, "Invalid request: "+err.Error())
  87. return
  88. }
  89. t, err := h.service.Create(c.Request.Context(), userID, req.Subject, req.Body)
  90. if err != nil {
  91. response.ErrorFrom(c, err)
  92. return
  93. }
  94. response.Success(c, t)
  95. }
  96. // GetMine 用户查看自己的工单。
  97. // GET /tickets/:id
  98. func (h *TicketHandler) GetMine(c *gin.Context) {
  99. if !h.requireService(c) {
  100. return
  101. }
  102. userID, ok := ticketUserID(c)
  103. if !ok {
  104. return
  105. }
  106. id, ok := ticketID(c)
  107. if !ok {
  108. return
  109. }
  110. t, err := h.service.GetMine(c.Request.Context(), userID, id)
  111. if err != nil {
  112. ticketErr(c, err)
  113. return
  114. }
  115. response.Success(c, t)
  116. }
  117. type ticketReplyRequest struct {
  118. Body string `json:"body"`
  119. }
  120. // ReplyMine 用户追问。
  121. // POST /tickets/:id/replies
  122. func (h *TicketHandler) ReplyMine(c *gin.Context) {
  123. if !h.requireService(c) {
  124. return
  125. }
  126. userID, ok := ticketUserID(c)
  127. if !ok {
  128. return
  129. }
  130. id, ok := ticketID(c)
  131. if !ok {
  132. return
  133. }
  134. var req ticketReplyRequest
  135. if err := c.ShouldBindJSON(&req); err != nil {
  136. response.BadRequest(c, "Invalid request: "+err.Error())
  137. return
  138. }
  139. r, err := h.service.ReplyMine(c.Request.Context(), userID, id, req.Body)
  140. if err != nil {
  141. ticketErr(c, err)
  142. return
  143. }
  144. response.Success(c, r)
  145. }
  146. // CloseMine 用户关闭。
  147. // POST /tickets/:id/close
  148. func (h *TicketHandler) CloseMine(c *gin.Context) {
  149. if !h.requireService(c) {
  150. return
  151. }
  152. userID, ok := ticketUserID(c)
  153. if !ok {
  154. return
  155. }
  156. id, ok := ticketID(c)
  157. if !ok {
  158. return
  159. }
  160. if err := h.service.CloseMine(c.Request.Context(), userID, id); err != nil {
  161. ticketErr(c, err)
  162. return
  163. }
  164. response.Success(c, gin.H{"message": "Ticket closed successfully"})
  165. }
  166. // ListAll 管理端列表:?status=&search=&limit=&offset=。
  167. // GET /api/v1/admin/tickets
  168. func (h *TicketHandler) ListAll(c *gin.Context) {
  169. if !h.requireService(c) {
  170. return
  171. }
  172. limit := 50
  173. if raw := c.Query("limit"); raw != "" {
  174. if v, err := strconv.Atoi(raw); err == nil && v > 0 && v <= 500 {
  175. limit = v
  176. }
  177. }
  178. offset := 0
  179. if raw := c.Query("offset"); raw != "" {
  180. if v, err := strconv.Atoi(raw); err == nil && v >= 0 {
  181. offset = v
  182. }
  183. }
  184. items, err := h.service.ListAll(c.Request.Context(), c.Query("status"), c.Query("search"), limit, offset)
  185. if err != nil {
  186. response.ErrorFrom(c, err)
  187. return
  188. }
  189. if items == nil {
  190. items = []service.SupportTicket{}
  191. }
  192. response.Success(c, gin.H{"items": items, "count": len(items)})
  193. }
  194. // GetAny 管理端查看。
  195. // GET /api/v1/admin/tickets/:id
  196. func (h *TicketHandler) GetAny(c *gin.Context) {
  197. if !h.requireService(c) {
  198. return
  199. }
  200. id, ok := ticketID(c)
  201. if !ok {
  202. return
  203. }
  204. t, err := h.service.GetAny(c.Request.Context(), id)
  205. if err != nil {
  206. ticketErr(c, err)
  207. return
  208. }
  209. response.Success(c, t)
  210. }
  211. // ReplyAny 管理员回复(状态置 answered)。
  212. // POST /api/v1/admin/tickets/:id/replies
  213. func (h *TicketHandler) ReplyAny(c *gin.Context) {
  214. if !h.requireService(c) {
  215. return
  216. }
  217. subject, ok := middleware2.GetAuthSubjectFromContext(c)
  218. if !ok {
  219. response.Unauthorized(c, "Unauthorized")
  220. return
  221. }
  222. id, ok := ticketID(c)
  223. if !ok {
  224. return
  225. }
  226. var req ticketReplyRequest
  227. if err := c.ShouldBindJSON(&req); err != nil {
  228. response.BadRequest(c, "Invalid request: "+err.Error())
  229. return
  230. }
  231. r, err := h.service.ReplyAny(c.Request.Context(), subject.UserID, id, req.Body)
  232. if err != nil {
  233. ticketErr(c, err)
  234. return
  235. }
  236. response.Success(c, r)
  237. }
  238. // CloseAny 管理员关闭。
  239. // POST /api/v1/admin/tickets/:id/close
  240. func (h *TicketHandler) CloseAny(c *gin.Context) {
  241. if !h.requireService(c) {
  242. return
  243. }
  244. id, ok := ticketID(c)
  245. if !ok {
  246. return
  247. }
  248. if err := h.service.CloseAny(c.Request.Context(), id); err != nil {
  249. ticketErr(c, err)
  250. return
  251. }
  252. response.Success(c, gin.H{"message": "Ticket closed successfully"})
  253. }
  254. // Stats 未关闭工单数(管理端角标)。
  255. // GET /api/v1/admin/tickets/stats
  256. func (h *TicketHandler) Stats(c *gin.Context) {
  257. if !h.requireService(c) {
  258. return
  259. }
  260. n, err := h.service.OpenCount(c.Request.Context())
  261. if err != nil {
  262. response.ErrorFrom(c, err)
  263. return
  264. }
  265. response.Success(c, gin.H{"open": n})
  266. }