feat: add app user routes (logout, user-info, avatar, tickets)
- Logout: clear device sessions on logout - User info: return expiry/balance based on billing type - Avatar upload: support jpg/png/gif/webp, max 2MB - App tickets: create, reply, close tickets for app users - Add ClosedAt field to Ticket model Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -427,6 +427,7 @@ type Ticket struct {
|
|||||||
Status string `gorm:"size:20;default:open" json:"status"` // open, processing, resolved, closed
|
Status string `gorm:"size:20;default:open" json:"status"` // open, processing, resolved, closed
|
||||||
Priority string `gorm:"size:20;default:normal" json:"priority"` // low, normal, high, urgent
|
Priority string `gorm:"size:20;default:normal" json:"priority"` // low, normal, high, urgent
|
||||||
AssignedTo *uint `json:"assigned_to"` // 分配给的应用开发者ID或平台管理员ID
|
AssignedTo *uint `json:"assigned_to"` // 分配给的应用开发者ID或平台管理员ID
|
||||||
|
ClosedAt *time.Time `json:"closed_at"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ func SetupRoutes(r *gin.RouterGroup) {
|
|||||||
SetupPaymentRoutes(r)
|
SetupPaymentRoutes(r)
|
||||||
SetupAccountRoutes(r)
|
SetupAccountRoutes(r)
|
||||||
SetupDynamicRoutes(r)
|
SetupDynamicRoutes(r)
|
||||||
|
SetupUserRoutes(r)
|
||||||
|
SetupTicketRoutes(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetupAuthRoutes(r *gin.RouterGroup) {
|
func SetupAuthRoutes(r *gin.RouterGroup) {
|
||||||
|
|||||||
@@ -0,0 +1,282 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"verification-platform-backend/internal/database"
|
||||||
|
"verification-platform-backend/internal/model"
|
||||||
|
"verification-platform-backend/internal/service"
|
||||||
|
"verification-platform-backend/pkg/response"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetupTicketRoutes(r *gin.RouterGroup) {
|
||||||
|
r.GET("/tickets", handleAppGetTickets)
|
||||||
|
r.GET("/tickets/:id", handleAppGetTicket)
|
||||||
|
r.POST("/tickets", handleAppCreateTicket)
|
||||||
|
r.POST("/tickets/:id/reply", handleAppReplyTicket)
|
||||||
|
r.PUT("/tickets/:id/close", handleAppCloseTicket)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAppGetTickets(c *gin.Context) {
|
||||||
|
appKey := c.Param("appKey")
|
||||||
|
var app model.Application
|
||||||
|
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||||
|
response.Error(c, 404, "应用不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if service.GetApplicationDisabledStatus(app.ID) {
|
||||||
|
response.Error(c, 403, "该应用已被禁用")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, exists := c.Get("user_id")
|
||||||
|
if !exists {
|
||||||
|
response.Error(c, 401, "未授权")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||||
|
statusFilter := c.Query("status")
|
||||||
|
|
||||||
|
query := database.DB.Model(&model.Ticket{}).Where("user_id = ? AND application_id = ?", userID, app.ID)
|
||||||
|
|
||||||
|
if statusFilter != "" {
|
||||||
|
query = query.Where("status = ?", statusFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
query.Count(&total)
|
||||||
|
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
|
||||||
|
var tickets []model.Ticket
|
||||||
|
if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&tickets).Error; err != nil {
|
||||||
|
response.Error(c, 500, "获取工单列表失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, gin.H{
|
||||||
|
"tickets": tickets,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": pageSize,
|
||||||
|
"total_page": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAppGetTicket(c *gin.Context) {
|
||||||
|
appKey := c.Param("appKey")
|
||||||
|
var app model.Application
|
||||||
|
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||||
|
response.Error(c, 404, "应用不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if service.GetApplicationDisabledStatus(app.ID) {
|
||||||
|
response.Error(c, 403, "该应用已被禁用")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, exists := c.Get("user_id")
|
||||||
|
if !exists {
|
||||||
|
response.Error(c, 401, "未授权")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ticketID := c.Param("id")
|
||||||
|
var ticket model.Ticket
|
||||||
|
if err := database.DB.Where("id = ? AND user_id = ? AND application_id = ?", ticketID, userID, app.ID).
|
||||||
|
Preload("Replies").
|
||||||
|
First(&ticket).Error; err != nil {
|
||||||
|
response.Error(c, 404, "工单不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, gin.H{
|
||||||
|
"ticket": ticket,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAppCreateTicket(c *gin.Context) {
|
||||||
|
appKey := c.Param("appKey")
|
||||||
|
var app model.Application
|
||||||
|
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||||
|
response.Error(c, 404, "应用不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if service.GetApplicationDisabledStatus(app.ID) {
|
||||||
|
response.Error(c, 403, "该应用已被禁用")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, exists := c.Get("user_id")
|
||||||
|
if !exists {
|
||||||
|
response.Error(c, 401, "未授权")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Title string `json:"title" binding:"required"`
|
||||||
|
Content string `json:"content" binding:"required"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Priority string `json:"priority"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Type == "" {
|
||||||
|
req.Type = "other"
|
||||||
|
}
|
||||||
|
if req.Priority == "" {
|
||||||
|
req.Priority = "normal"
|
||||||
|
}
|
||||||
|
|
||||||
|
ticket := model.Ticket{
|
||||||
|
UserID: userID.(uint),
|
||||||
|
ApplicationID: &app.ID,
|
||||||
|
Title: req.Title,
|
||||||
|
Content: req.Content,
|
||||||
|
Type: req.Type,
|
||||||
|
Category: "other",
|
||||||
|
Priority: req.Priority,
|
||||||
|
Status: "open",
|
||||||
|
AssignedTo: &app.UserID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.DB.Create(&ticket).Error; err != nil {
|
||||||
|
log.Printf("[ERROR] Failed to create ticket: %v", err)
|
||||||
|
response.Error(c, 500, "创建工单失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
service.LogOperation(c, "create", "ticket", &ticket.ID, fmt.Sprintf("用户创建工单: %s", ticket.Title), nil)
|
||||||
|
|
||||||
|
response.Success(c, gin.H{
|
||||||
|
"id": ticket.ID,
|
||||||
|
"message": "工单创建成功",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAppReplyTicket(c *gin.Context) {
|
||||||
|
appKey := c.Param("appKey")
|
||||||
|
var app model.Application
|
||||||
|
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||||
|
response.Error(c, 404, "应用不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if service.GetApplicationDisabledStatus(app.ID) {
|
||||||
|
response.Error(c, 403, "该应用已被禁用")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, exists := c.Get("user_id")
|
||||||
|
if !exists {
|
||||||
|
response.Error(c, 401, "未授权")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ticketID := c.Param("id")
|
||||||
|
var ticket model.Ticket
|
||||||
|
if err := database.DB.Where("id = ? AND user_id = ? AND application_id = ?", ticketID, userID, app.ID).First(&ticket).Error; err != nil {
|
||||||
|
response.Error(c, 404, "工单不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ticket.Status == "closed" {
|
||||||
|
response.Error(c, 400, "工单已关闭,无法回复")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
Content string `json:"content" binding:"required"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reply := model.TicketReply{
|
||||||
|
TicketID: parseUint(ticketID),
|
||||||
|
UserID: userID.(uint),
|
||||||
|
Content: req.Content,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.DB.Create(&reply).Error; err != nil {
|
||||||
|
response.Error(c, 500, "回复失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ticket.Status == "resolved" || ticket.Status == "processing" {
|
||||||
|
ticket.Status = "processing"
|
||||||
|
database.DB.Save(&ticket)
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, gin.H{
|
||||||
|
"id": reply.ID,
|
||||||
|
"message": "回复成功",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAppCloseTicket(c *gin.Context) {
|
||||||
|
appKey := c.Param("appKey")
|
||||||
|
var app model.Application
|
||||||
|
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||||
|
response.Error(c, 404, "应用不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if service.GetApplicationDisabledStatus(app.ID) {
|
||||||
|
response.Error(c, 403, "该应用已被禁用")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, exists := c.Get("user_id")
|
||||||
|
if !exists {
|
||||||
|
response.Error(c, 401, "未授权")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ticketID := c.Param("id")
|
||||||
|
var ticket model.Ticket
|
||||||
|
if err := database.DB.Where("id = ? AND user_id = ? AND application_id = ?", ticketID, userID, app.ID).First(&ticket).Error; err != nil {
|
||||||
|
response.Error(c, 404, "工单不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ticket.Status == "closed" {
|
||||||
|
response.Error(c, 400, "工单已关闭")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
ticket.Status = "closed"
|
||||||
|
ticket.ClosedAt = &now
|
||||||
|
|
||||||
|
if err := database.DB.Save(&ticket).Error; err != nil {
|
||||||
|
response.Error(c, 500, "关闭工单失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
service.LogOperation(c, "close", "ticket", &ticket.ID, fmt.Sprintf("用户关闭工单: %s", ticket.Title), nil)
|
||||||
|
|
||||||
|
response.Success(c, gin.H{
|
||||||
|
"message": "工单已关闭",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUint(s string) uint {
|
||||||
|
var val uint
|
||||||
|
fmt.Sscanf(s, "%d", &val)
|
||||||
|
return val
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"verification-platform-backend/internal/database"
|
||||||
|
"verification-platform-backend/internal/model"
|
||||||
|
"verification-platform-backend/internal/service"
|
||||||
|
"verification-platform-backend/pkg/response"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetupUserRoutes(r *gin.RouterGroup) {
|
||||||
|
r.POST("/logout", handleAppLogout)
|
||||||
|
r.GET("/user-info", handleAppGetUserInfo)
|
||||||
|
r.POST("/avatar", handleAppUploadAvatar)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAppLogout(c *gin.Context) {
|
||||||
|
appKey := c.Param("appKey")
|
||||||
|
var app model.Application
|
||||||
|
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||||
|
response.Error(c, 404, "应用不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if service.GetApplicationDisabledStatus(app.ID) {
|
||||||
|
response.Error(c, 403, "该应用已被禁用")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, exists := c.Get("user_id")
|
||||||
|
if !exists {
|
||||||
|
response.Error(c, 401, "未授权")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req struct {
|
||||||
|
DeviceID string `json:"device_id"`
|
||||||
|
InstanceID string `json:"instance_id"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.Error(c, 400, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.DeviceID != "" {
|
||||||
|
var device model.UserDevice
|
||||||
|
if err := database.DB.Where("user_id = ? AND application_id = ? AND device_id = ?", userID, app.ID, req.DeviceID).First(&device).Error; err == nil {
|
||||||
|
if req.InstanceID != "" {
|
||||||
|
database.DB.Where("device_id = ? AND instance_id = ?", device.ID, req.InstanceID).Delete(&model.DeviceSession{})
|
||||||
|
log.Printf("[INFO] Logout: deleted session for device %s, instance %s", req.DeviceID, req.InstanceID)
|
||||||
|
} else {
|
||||||
|
database.DB.Where("device_id = ?", device.ID).Delete(&model.DeviceSession{})
|
||||||
|
log.Printf("[INFO] Logout: deleted all sessions for device %s", req.DeviceID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service.LogVerification(c, &app.ID, uintPtr(userID.(uint)), "logout", "用户登出", req.DeviceID, nil)
|
||||||
|
|
||||||
|
response.Success(c, gin.H{
|
||||||
|
"message": "登出成功",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAppGetUserInfo(c *gin.Context) {
|
||||||
|
appKey := c.Param("appKey")
|
||||||
|
var app model.Application
|
||||||
|
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||||
|
response.Error(c, 404, "应用不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if service.GetApplicationDisabledStatus(app.ID) {
|
||||||
|
response.Error(c, 403, "该应用已被禁用")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, exists := c.Get("user_id")
|
||||||
|
if !exists {
|
||||||
|
response.Error(c, 401, "未授权")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var user model.AppUser
|
||||||
|
if err := database.DB.First(&user, userID).Error; err != nil {
|
||||||
|
response.Error(c, 404, "用户不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := gin.H{
|
||||||
|
"user_id": user.ID,
|
||||||
|
"username": user.Username,
|
||||||
|
"email": user.Email,
|
||||||
|
"avatar": user.Avatar,
|
||||||
|
"status": user.Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Balance == -1 {
|
||||||
|
result["is_permanent"] = true
|
||||||
|
} else if app.BillingType == "subscription" {
|
||||||
|
if user.ExpiryAt != nil {
|
||||||
|
result["expiry_at"] = user.ExpiryAt.Format("2006-01-02 15:04:05")
|
||||||
|
result["expiry_timestamp"] = user.ExpiryAt.Unix()
|
||||||
|
}
|
||||||
|
} else if app.BillingType != "free" {
|
||||||
|
result["balance"] = user.Balance
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAppUploadAvatar(c *gin.Context) {
|
||||||
|
appKey := c.Param("appKey")
|
||||||
|
var app model.Application
|
||||||
|
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||||
|
response.Error(c, 404, "应用不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if service.GetApplicationDisabledStatus(app.ID) {
|
||||||
|
response.Error(c, 403, "该应用已被禁用")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, exists := c.Get("user_id")
|
||||||
|
if !exists {
|
||||||
|
response.Error(c, 401, "未授权")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, header, err := c.Request.FormFile("avatar")
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, 400, "请上传头像文件")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if !isValidImageFile(header.Filename) {
|
||||||
|
response.Error(c, 400, "只支持JPG、PNG、GIF格式的图片")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxSize = 2 << 20
|
||||||
|
if header.Size > maxSize {
|
||||||
|
response.Error(c, 400, "头像文件大小不能超过2MB")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
avatarDir := filepath.Join("uploads", "avatars")
|
||||||
|
if err := os.MkdirAll(avatarDir, 0755); err != nil {
|
||||||
|
response.Error(c, 500, "创建目录失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := filepath.Ext(header.Filename)
|
||||||
|
filename := fmt.Sprintf("avatar_%d_%s%s", userID.(uint), uuid.New().String()[:8], ext)
|
||||||
|
dst := filepath.Join(avatarDir, filename)
|
||||||
|
|
||||||
|
dstFile, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, 500, "创建文件失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer dstFile.Close()
|
||||||
|
|
||||||
|
if _, err := io.Copy(dstFile, file); err != nil {
|
||||||
|
response.Error(c, 500, "保存文件失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
avatarURL := fmt.Sprintf("/uploads/avatars/%s", filename)
|
||||||
|
|
||||||
|
var user model.AppUser
|
||||||
|
if err := database.DB.First(&user, userID).Error; err != nil {
|
||||||
|
response.Error(c, 404, "用户不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
oldAvatar := user.Avatar
|
||||||
|
user.Avatar = avatarURL
|
||||||
|
if err := database.DB.Save(&user).Error; err != nil {
|
||||||
|
response.Error(c, 500, "保存头像失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldAvatar != "" && strings.HasPrefix(oldAvatar, "/uploads/avatars/") {
|
||||||
|
oldPath := filepath.Join(".", oldAvatar)
|
||||||
|
os.Remove(oldPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, gin.H{
|
||||||
|
"avatar_url": avatarURL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidImageFile(filename string) bool {
|
||||||
|
ext := strings.ToLower(filepath.Ext(filename))
|
||||||
|
validExts := map[string]bool{
|
||||||
|
".jpg": true,
|
||||||
|
".jpeg": true,
|
||||||
|
".png": true,
|
||||||
|
".gif": true,
|
||||||
|
".webp": true,
|
||||||
|
}
|
||||||
|
return validExts[ext]
|
||||||
|
}
|
||||||
|
|
||||||
|
func uintPtr(v uint) *uint {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user