Files
SingBox-Gopanel/internal/handler/user_support_api.go
CN-JS-HuiBai 1ed31b9292
All checks were successful
build / build (api, amd64, linux) (push) Successful in -47s
build / build (api, arm64, linux) (push) Successful in -48s
build / build (api.exe, amd64, windows) (push) Successful in -47s
first commit
2026-04-17 09:49:16 +08:00

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 ""
}