package handler import ( "net/http" "slices" "strings" "time" "xboard-go/internal/database" "xboard-go/internal/model" "xboard-go/internal/service" "github.com/gin-gonic/gin" ) func UserKnowledgeFetch(c *gin.Context) { language := strings.TrimSpace(c.DefaultQuery("language", "zh-CN")) query := database.DB.Model(&model.Knowledge{}).Where("`show` = ?", 1).Order("sort ASC, id ASC") if language != "" { query = query.Where("language = ? OR language = ''", language) } var articles []model.Knowledge if err := query.Find(&articles).Error; err != nil { Fail(c, 500, "failed to fetch knowledge articles") return } Success(c, articles) } func UserKnowledgeCategories(c *gin.Context) { var categories []string if err := database.DB.Model(&model.Knowledge{}). Where("`show` = ?", 1). Distinct(). Order("category ASC"). Pluck("category", &categories).Error; err != nil { Fail(c, 500, "failed to fetch knowledge categories") return } filtered := make([]string, 0, len(categories)) for _, category := range categories { category = strings.TrimSpace(category) if category != "" && !slices.Contains(filtered, category) { filtered = append(filtered, category) } } Success(c, filtered) } func UserTicketFetch(c *gin.Context) { user, ok := currentUser(c) if !ok { Fail(c, http.StatusUnauthorized, "user not found") return } if ticketID := strings.TrimSpace(c.Query("id")); ticketID != "" { var ticket model.Ticket if err := database.DB.Where("id = ? AND user_id = ?", ticketID, user.ID).First(&ticket).Error; err != nil { Fail(c, 404, "ticket not found") return } var messages []model.TicketMessage _ = database.DB.Where("ticket_id = ?", ticket.ID).Order("id ASC").Find(&messages).Error payload := gin.H{ "id": ticket.ID, "user_id": ticket.UserID, "subject": ticket.Subject, "level": ticket.Level, "status": ticket.Status, "reply_status": ticket.ReplyStatus, "created_at": ticket.CreatedAt, "updated_at": ticket.UpdatedAt, "message": buildTicketMessages(messages, user.ID), } Success(c, payload) return } var tickets []model.Ticket if err := database.DB.Where("user_id = ?", user.ID).Order("updated_at DESC, id DESC").Find(&tickets).Error; err != nil { Fail(c, 500, "failed to fetch tickets") return } Success(c, tickets) } func UserTicketSave(c *gin.Context) { user, ok := currentUser(c) if !ok { Fail(c, http.StatusUnauthorized, "user not found") return } var req struct { Subject string `json:"subject" binding:"required"` Level int `json:"level"` Message string `json:"message" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { Fail(c, 400, "invalid ticket payload") return } now := time.Now().Unix() ticket := model.Ticket{ UserID: user.ID, Subject: strings.TrimSpace(req.Subject), Level: req.Level, Status: 0, ReplyStatus: 1, CreatedAt: now, UpdatedAt: now, } if err := database.DB.Create(&ticket).Error; err != nil { Fail(c, 500, "failed to create ticket") return } message := model.TicketMessage{ UserID: user.ID, TicketID: ticket.ID, Message: strings.TrimSpace(req.Message), CreatedAt: now, UpdatedAt: now, } if err := database.DB.Create(&message).Error; err != nil { Fail(c, 500, "failed to save ticket message") return } SuccessMessage(c, "ticket created", true) } func UserTicketReply(c *gin.Context) { user, ok := currentUser(c) if !ok { Fail(c, http.StatusUnauthorized, "user not found") return } var req struct { ID int `json:"id" binding:"required"` Message string `json:"message" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { Fail(c, 400, "invalid ticket reply payload") return } var ticket model.Ticket if err := database.DB.Where("id = ? AND user_id = ?", req.ID, user.ID).First(&ticket).Error; err != nil { Fail(c, 404, "ticket not found") return } if ticket.Status != 0 { Fail(c, 400, "ticket is closed") return } now := time.Now().Unix() reply := model.TicketMessage{ UserID: user.ID, TicketID: ticket.ID, Message: strings.TrimSpace(req.Message), CreatedAt: now, UpdatedAt: now, } if err := database.DB.Create(&reply).Error; err != nil { Fail(c, 500, "failed to save ticket reply") return } _ = database.DB.Model(&model.Ticket{}). Where("id = ?", ticket.ID). Updates(map[string]any{"reply_status": 1, "updated_at": now}).Error SuccessMessage(c, "ticket replied", true) } func UserTicketClose(c *gin.Context) { updateTicketStatus(c, 1, "ticket closed") } func UserTicketWithdraw(c *gin.Context) { updateTicketStatus(c, 1, "ticket withdrawn") } func UserGetActiveSession(c *gin.Context) { user, ok := currentUser(c) if !ok { Fail(c, http.StatusUnauthorized, "user not found") return } authToken, _ := c.Get("auth_token") currentToken, _ := authToken.(string) sessions := service.GetUserSessions(user.ID, currentToken) payload := make([]gin.H, 0, len(sessions)) currentSessionID := currentSessionID(c) for _, session := range sessions { payload = append(payload, gin.H{ "id": session.ID, "name": session.Name, "user_agent": session.UserAgent, "ip": firstString(session.IP, "-"), "created_at": session.CreatedAt, "last_used_at": session.LastUsedAt, "expires_at": session.ExpiresAt, "is_current": session.ID == currentSessionID, }) } Success(c, payload) } func UserRemoveActiveSession(c *gin.Context) { user, ok := currentUser(c) if !ok { Fail(c, http.StatusUnauthorized, "user not found") return } var req struct { SessionID string `json:"session_id" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { Fail(c, 400, "session_id is required") return } if !service.RemoveUserSession(user.ID, req.SessionID) { Fail(c, 404, "session not found") return } SuccessMessage(c, "session removed", true) } func updateTicketStatus(c *gin.Context, status int, message string) { user, ok := currentUser(c) if !ok { Fail(c, http.StatusUnauthorized, "user not found") return } var req struct { ID int `json:"id" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { Fail(c, 400, "ticket id is required") return } now := time.Now().Unix() result := database.DB.Model(&model.Ticket{}). Where("id = ? AND user_id = ?", req.ID, user.ID). Updates(map[string]any{ "status": status, "updated_at": now, }) if result.Error != nil { Fail(c, 500, "failed to update ticket") return } if result.RowsAffected == 0 { Fail(c, 404, "ticket not found") return } SuccessMessage(c, message, true) } func buildTicketMessages(messages []model.TicketMessage, currentUserID int) []gin.H { payload := make([]gin.H, 0, len(messages)) for _, message := range messages { payload = append(payload, gin.H{ "id": message.ID, "user_id": message.UserID, "message": message.Message, "created_at": message.CreatedAt, "updated_at": message.UpdatedAt, "is_me": message.UserID == currentUserID, }) } return payload } func currentSessionID(c *gin.Context) string { value, exists := c.Get("session") if !exists { return "" } session, ok := value.(service.SessionRecord) if !ok { return "" } return session.ID } func firstString(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return value } } return "" }