314 lines
7.4 KiB
Go
314 lines
7.4 KiB
Go
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 ""
|
|
}
|