first commit
This commit is contained in:
313
internal/handler/user_support_api.go
Normal file
313
internal/handler/user_support_api.go
Normal file
@@ -0,0 +1,313 @@
|
||||
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 ""
|
||||
}
|
||||
Reference in New Issue
Block a user