package handler import ( "net/http" "strings" "time" "xboard-go/internal/database" "xboard-go/internal/model" "xboard-go/internal/service" "github.com/gin-gonic/gin" ) func AdminDashboardSummary(c *gin.Context) { now := time.Now().Unix() todayStart := time.Now().Truncate(24 * time.Hour).Unix() yesterdayStart := todayStart - 86400 currentMonthStart := time.Date(time.Now().Year(), time.Now().Month(), 1, 0, 0, 0, 0, time.Local).Unix() lastMonthStart := time.Date(time.Now().Year(), time.Now().Month()-1, 1, 0, 0, 0, 0, time.Local).Unix() // 1. Basic Counts var totalUsers int64 var totalOrders int64 var pendingOrders int64 var pendingTickets int64 var commissionPendingTotal int64 var onlineUsers int64 var onlineNodes int64 var onlineDevices int64 database.DB.Model(&model.User{}).Count(&totalUsers) database.DB.Model(&model.Order{}).Count(&totalOrders) database.DB.Model(&model.Order{}).Where("status = ?", 0).Count(&pendingOrders) database.DB.Model(&model.Order{}). Where("status = ? AND commission_status = ?", 3, 0). Select("COALESCE(SUM(commission_balance), 0)"). Scan(&commissionPendingTotal) database.DB.Model(&model.Ticket{}).Where("status = ?", 0).Count(&pendingTickets) database.DB.Model(&model.Server{}).Where("show = ?", true).Count(&onlineNodes) // Simplified online check // Online status (last 10 mins) tenMinsAgo := now - 600 database.DB.Model(&model.User{}).Where("t >= ?", tenMinsAgo).Count(&onlineUsers) database.DB.Model(&model.User{}).Where("t >= ?", tenMinsAgo).Select("COALESCE(SUM(online_count), 0)").Scan(&onlineDevices) // 2. Income Statistics var todayIncome int64 var yesterdayIncome int64 var currentMonthIncome int64 var lastMonthIncome int64 database.DB.Model(&model.Order{}).Where("status NOT IN ? AND created_at >= ?", []int{0, 2}, todayStart).Select("COALESCE(SUM(total_amount), 0)").Scan(&todayIncome) database.DB.Model(&model.Order{}).Where("status NOT IN ? AND created_at >= ? AND created_at < ?", []int{0, 2}, yesterdayStart, todayStart).Select("COALESCE(SUM(total_amount), 0)").Scan(&yesterdayIncome) database.DB.Model(&model.Order{}).Where("status NOT IN ? AND created_at >= ?", []int{0, 2}, currentMonthStart).Select("COALESCE(SUM(total_amount), 0)").Scan(¤tMonthIncome) database.DB.Model(&model.Order{}).Where("status NOT IN ? AND created_at >= ? AND created_at < ?", []int{0, 2}, lastMonthStart, currentMonthStart).Select("COALESCE(SUM(total_amount), 0)").Scan(&lastMonthIncome) // 3. Traffic Statistics (using StatServer) type trafficSum struct { U int64 `json:"upload"` D int64 `json:"download"` Total int64 `json:"total"` } var todayTraffic trafficSum var monthTraffic trafficSum var totalTraffic trafficSum database.DB.Model(&model.StatServer{}).Where("record_at >= ?", todayStart).Select("COALESCE(SUM(u), 0) as u, COALESCE(SUM(d), 0) as d, COALESCE(SUM(u+d), 0) as total").Scan(&todayTraffic) database.DB.Model(&model.StatServer{}).Where("record_at >= ?", currentMonthStart).Select("COALESCE(SUM(u), 0) as u, COALESCE(SUM(d), 0) as d, COALESCE(SUM(u+d), 0) as total").Scan(&monthTraffic) database.DB.Model(&model.StatServer{}).Select("COALESCE(SUM(u), 0) as u, COALESCE(SUM(d), 0) as d, COALESCE(SUM(u+d), 0) as total").Scan(&totalTraffic) // 4. User Growth var currentMonthNewUsers int64 var lastMonthNewUsers int64 database.DB.Model(&model.User{}).Where("created_at >= ?", currentMonthStart).Count(¤tMonthNewUsers) database.DB.Model(&model.User{}).Where("created_at >= ? AND created_at < ?", lastMonthStart, currentMonthStart).Count(&lastMonthNewUsers) // Calculate Growth Rates dayIncomeGrowth := calculateGrowth(todayIncome, yesterdayIncome) monthIncomeGrowth := calculateGrowth(currentMonthIncome, lastMonthIncome) userGrowth := calculateGrowth(currentMonthNewUsers, lastMonthNewUsers) Success(c, gin.H{ "server_time": now, "todayIncome": todayIncome, "dayIncomeGrowth": dayIncomeGrowth, "currentMonthIncome": currentMonthIncome, "lastMonthIncome": lastMonthIncome, "monthIncomeGrowth": monthIncomeGrowth, "totalOrders": totalOrders, "pendingOrders": pendingOrders, "currentMonthNewUsers": currentMonthNewUsers, "totalUsers": totalUsers, "activeUsers": totalUsers, // Placeholder for valid subscription count "userGrowth": userGrowth, "commissionPendingTotal": commissionPendingTotal, "onlineUsers": onlineUsers, "onlineDevices": onlineDevices, "ticketPendingTotal": pendingTickets, "onlineNodes": onlineNodes, "todayTraffic": todayTraffic, "monthTraffic": monthTraffic, "totalTraffic": totalTraffic, "secure_path": service.GetAdminSecurePath(), "app_name": service.MustGetString("app_name", "XBoard"), }) } func calculateGrowth(current, previous int64) float64 { if previous <= 0 { if current > 0 { return 100.0 } return 0.0 } return float64(current-previous) / float64(previous) * 100.0 } func AdminPlansFetch(c *gin.Context) { var plans []model.Plan if err := database.DB.Order("sort ASC, id DESC").Find(&plans).Error; err != nil { Fail(c, http.StatusInternalServerError, "failed to fetch plans") return } groupNames := loadServerGroupNameMap() items := make([]gin.H, 0, len(plans)) for _, plan := range plans { items = append(items, gin.H{ "id": plan.ID, "name": plan.Name, "group_id": intValue(plan.GroupID), "group_name": groupNames[intFromPointer(plan.GroupID)], "transfer_enable": intValue(plan.TransferEnable), "speed_limit": intValue(plan.SpeedLimit), "show": plan.Show, "sort": intValue(plan.Sort), "renew": plan.Renew, "content": stringValue(plan.Content), "reset_traffic_method": intValue(plan.ResetTrafficMethod), "capacity_limit": intValue(plan.CapacityLimit), "prices": stringValue(plan.Prices), "sell": plan.Sell, "device_limit": intValue(plan.DeviceLimit), "tags": parseStringSlice(plan.Tags), "created_at": plan.CreatedAt, "updated_at": plan.UpdatedAt, }) } Success(c, items) } func AdminPlanSave(c *gin.Context) { var payload map[string]any if err := c.ShouldBindJSON(&payload); err != nil { Fail(c, http.StatusBadRequest, "invalid request body") return } id := intFromAny(payload["id"]) name := strings.TrimSpace(stringFromAny(payload["name"])) if name == "" && id == 0 { Fail(c, http.StatusUnprocessableEntity, "plan name is required") return } // For simplicity in this Go implementation, we'll map payload to model.Plan // In a real app, we'd use a dedicated struct for binding. now := time.Now().Unix() tags, _ := marshalJSON(payload["tags"], true) values := map[string]any{ "name": payload["name"], "group_id": payload["group_id"], "transfer_enable": payload["transfer_enable"], "speed_limit": payload["speed_limit"], "show": payload["show"], "sort": payload["sort"], "renew": payload["renew"], "content": payload["content"], "reset_traffic_method": payload["reset_traffic_method"], "capacity_limit": payload["capacity_limit"], "prices": payload["prices"], "sell": payload["sell"], "device_limit": payload["device_limit"], "tags": tags, "updated_at": now, } if id > 0 { if err := database.DB.Model(&model.Plan{}).Where("id = ?", id).Updates(values).Error; err != nil { Fail(c, http.StatusInternalServerError, "failed to save plan") return } Success(c, true) return } values["created_at"] = now if err := database.DB.Model(&model.Plan{}).Create(values).Error; err != nil { Fail(c, http.StatusInternalServerError, "failed to create plan") return } Success(c, true) } func AdminPlanDrop(c *gin.Context) { var payload struct { ID int `json:"id"` } if err := c.ShouldBindJSON(&payload); err != nil || payload.ID <= 0 { Fail(c, http.StatusBadRequest, "plan id is required") return } // Check usage var orderCount int64 database.DB.Model(&model.Order{}).Where("plan_id = ?", payload.ID).Count(&orderCount) if orderCount > 0 { Fail(c, http.StatusBadRequest, "this plan is still used by orders") return } var userCount int64 database.DB.Model(&model.User{}).Where("plan_id = ?", payload.ID).Count(&userCount) if userCount > 0 { Fail(c, http.StatusBadRequest, "this plan is still used by users") return } if err := database.DB.Where("id = ?", payload.ID).Delete(&model.Plan{}).Error; err != nil { Fail(c, http.StatusInternalServerError, "failed to delete plan") return } Success(c, true) } func AdminPlanSort(c *gin.Context) { var payload struct { PlanIDs []int `json:"plan_ids"` } if err := c.ShouldBindJSON(&payload); err != nil { Fail(c, http.StatusBadRequest, "invalid request body") return } tx := database.DB.Begin() for i, id := range payload.PlanIDs { if err := tx.Model(&model.Plan{}).Where("id = ?", id).Update("sort", i+1).Error; err != nil { tx.Rollback() Fail(c, http.StatusInternalServerError, "failed to sort plans") return } } tx.Commit() Success(c, true) } func AdminOrdersFetch(c *gin.Context) { params := getFetchParams(c) page := parsePositiveInt(firstString(params["page"], params["current"]), 1) perPage := parsePositiveInt(firstString(params["per_page"], params["pageSize"]), 50) keyword := strings.TrimSpace(params["keyword"]) statusFilter := strings.TrimSpace(params["status"]) query := database.DB.Model(&model.Order{}).Preload("Plan").Preload("Payment").Order("id DESC") if keyword != "" { query = query.Where("trade_no LIKE ? OR CAST(user_id AS CHAR) = ?", "%"+keyword+"%", keyword) } if statusFilter != "" { query = query.Where("status = ?", statusFilter) } var total int64 if err := query.Count(&total).Error; err != nil { Fail(c, http.StatusInternalServerError, "failed to count orders") return } var orders []model.Order if err := query.Offset((page - 1) * perPage).Limit(perPage).Find(&orders).Error; err != nil { Fail(c, http.StatusInternalServerError, "failed to fetch orders") return } userEmails := loadUserEmailMap(extractOrderUserIDs(orders)) inviteEmails := loadUserEmailMap(extractOrderInviteUserIDs(orders)) items := make([]gin.H, 0, len(orders)) for _, order := range orders { item := normalizeOrder(order) item["user_email"] = userEmails[order.UserID] item["user"] = gin.H{ "id": order.UserID, "email": userEmails[order.UserID], } if order.InviteUserID != nil { item["invite_user"] = gin.H{ "id": *order.InviteUserID, "email": inviteEmails[*order.InviteUserID], } } items = append(items, item) } Success(c, gin.H{ "list": items, "data": items, "total": total, "filters": gin.H{ "keyword": keyword, "status": statusFilter, }, "pagination": gin.H{ "current": page, "last_page": calculateLastPage(total, perPage), "per_page": perPage, "total": total, }, }) } func AdminCouponsFetch(c *gin.Context) { var coupons []model.Coupon if err := database.DB.Order("id DESC").Find(&coupons).Error; err != nil { Fail(c, http.StatusInternalServerError, "failed to fetch coupons") return } items := make([]gin.H, 0, len(coupons)) for _, coupon := range coupons { items = append(items, gin.H{ "id": coupon.ID, "name": coupon.Name, "code": coupon.Code, "type": coupon.Type, "value": coupon.Value, "limit_plan_ids": parseStringSlice(coupon.LimitPlanIDs), "limit_period": parseStringSlice(coupon.LimitPeriod), "limit_use": intValue(coupon.LimitUse), "limit_use_with_user": intValue(coupon.LimitUseWithUser), "started_at": coupon.StartedAt, "ended_at": coupon.EndedAt, "show": coupon.Show, "created_at": coupon.CreatedAt, "updated_at": coupon.UpdatedAt, }) } Success(c, items) } func AdminCouponSave(c *gin.Context) { var payload map[string]any if err := c.ShouldBindJSON(&payload); err != nil { Fail(c, http.StatusBadRequest, "invalid request body") return } id := intFromAny(payload["id"]) now := time.Now().Unix() limitPlanIDs, _ := marshalJSON(payload["limit_plan_ids"], true) limitPeriod, _ := marshalJSON(payload["limit_period"], true) values := map[string]any{ "name": payload["name"], "code": payload["code"], "type": payload["type"], "value": payload["value"], "limit_plan_ids": limitPlanIDs, "limit_period": limitPeriod, "limit_use": payload["limit_use"], "limit_use_with_user": payload["limit_use_with_user"], "started_at": payload["started_at"], "ended_at": payload["ended_at"], "show": payload["show"], "updated_at": now, } if id > 0 { if err := database.DB.Model(&model.Coupon{}).Where("id = ?", id).Updates(values).Error; err != nil { Fail(c, http.StatusInternalServerError, "failed to save coupon") return } Success(c, true) return } values["created_at"] = now if err := database.DB.Model(&model.Coupon{}).Create(values).Error; err != nil { Fail(c, http.StatusInternalServerError, "failed to create coupon") return } Success(c, true) } func AdminCouponDrop(c *gin.Context) { var payload struct { ID int `json:"id"` } if err := c.ShouldBindJSON(&payload); err != nil || payload.ID <= 0 { Fail(c, http.StatusBadRequest, "coupon id is required") return } if err := database.DB.Where("id = ?", payload.ID).Delete(&model.Coupon{}).Error; err != nil { Fail(c, http.StatusInternalServerError, "failed to delete coupon") return } Success(c, true) } func AdminOrderPaid(c *gin.Context) { var payload struct { TradeNo string `json:"trade_no"` } if err := c.ShouldBindJSON(&payload); err != nil || payload.TradeNo == "" { Fail(c, http.StatusBadRequest, "trade_no is required") return } var order model.Order if err := database.DB.Preload("Plan").Where("trade_no = ?", payload.TradeNo).First(&order).Error; err != nil { Fail(c, http.StatusBadRequest, "order does not exist") return } if order.Status != 0 { Fail(c, http.StatusBadRequest, "order status is not pending") return } now := time.Now().Unix() tx := database.DB.Begin() // Update order if err := tx.Model(&model.Order{}).Where("id = ?", order.ID).Updates(map[string]any{ "status": 3, "paid_at": now, }).Error; err != nil { tx.Rollback() Fail(c, http.StatusInternalServerError, "failed to update order") return } updates := map[string]any{ "plan_id": order.PlanID, "updated_at": now, } if order.Plan != nil { updates["transfer_enable"] = order.Plan.TransferEnable } if err := tx.Model(&model.User{}).Where("id = ?", order.UserID).Updates(updates).Error; err != nil { tx.Rollback() Fail(c, http.StatusInternalServerError, "failed to update user") return } tx.Commit() Success(c, true) } func AdminOrderCancel(c *gin.Context) { var payload struct { TradeNo string `json:"trade_no"` } if err := c.ShouldBindJSON(&payload); err != nil || payload.TradeNo == "" { Fail(c, http.StatusBadRequest, "trade_no is required") return } if err := database.DB.Model(&model.Order{}).Where("trade_no = ?", payload.TradeNo).Update("status", 2).Error; err != nil { Fail(c, http.StatusInternalServerError, "failed to cancel order") return } Success(c, true) } func AdminUserResetTraffic(c *gin.Context) { var payload struct { ID int `json:"id"` } if err := c.ShouldBindJSON(&payload); err != nil || payload.ID <= 0 { Fail(c, http.StatusBadRequest, "user id is required") return } if err := database.DB.Model(&model.User{}).Where("id = ?", payload.ID).Updates(map[string]any{ "u": 0, "d": 0, }).Error; err != nil { Fail(c, http.StatusInternalServerError, "failed to reset traffic") return } Success(c, true) } func AdminUserBan(c *gin.Context) { var payload struct { ID int `json:"id"` Banned bool `json:"banned"` } if err := c.ShouldBindJSON(&payload); err != nil || payload.ID <= 0 { Fail(c, http.StatusBadRequest, "user id is required") return } if err := database.DB.Model(&model.User{}).Where("id = ?", payload.ID).Update("banned", payload.Banned).Error; err != nil { Fail(c, http.StatusInternalServerError, "failed to update user status") return } Success(c, true) } func AdminUserDelete(c *gin.Context) { var payload struct { ID int `json:"id"` } if err := c.ShouldBindJSON(&payload); err != nil || payload.ID <= 0 { Fail(c, http.StatusBadRequest, "user id is required") return } if err := database.DB.Where("id = ?", payload.ID).Delete(&model.User{}).Error; err != nil { Fail(c, http.StatusInternalServerError, "failed to delete user") return } Success(c, true) } func AdminUserUpdate(c *gin.Context) { var payload map[string]any if err := c.ShouldBindJSON(&payload); err != nil { Fail(c, http.StatusBadRequest, "invalid request body") return } id := intFromAny(payload["id"]) if id <= 0 { Fail(c, http.StatusBadRequest, "user id is required") return } now := time.Now().Unix() values := map[string]any{ "email": payload["email"], "password": payload["password"], "balance": payload["balance"], "commission_type": payload["commission_type"], "commission_rate": payload["commission_rate"], "commission_balance": payload["commission_balance"], "group_id": payload["group_id"], "plan_id": payload["plan_id"], "speed_limit": payload["speed_limit"], "device_limit": payload["device_limit"], "expired_at": payload["expired_at"], "remarks": payload["remarks"], "updated_at": now, } // Remove nil values to avoid overwriting with defaults if not provided for k, v := range values { if v == nil { delete(values, k) } } if err := database.DB.Model(&model.User{}).Where("id = ?", id).Updates(values).Error; err != nil { Fail(c, http.StatusInternalServerError, "failed to update user") return } // Sync password to children (IPv6 sub-users) if changed if pwd, ok := values["password"].(string); ok && pwd != "" { database.DB.Model(&model.User{}).Where("parent_id = ?", id).Update("password", pwd) } Success(c, true) } func AdminUsersFetch(c *gin.Context) { params := getFetchParams(c) page := parsePositiveInt(firstString(params["page"], params["current"]), 1) perPage := parsePositiveInt(firstString(params["per_page"], params["pageSize"]), 50) keyword := strings.TrimSpace(params["keyword"]) suffix := service.GetPluginConfigString(service.PluginUserAddIPv6, "email_suffix", "-ipv6") shadowPattern := "%" + suffix + "@%" query := database.DB.Model(&model.User{}). Preload("Plan"). Where("email NOT LIKE ?", shadowPattern). Order("id DESC") if keyword != "" { query = query.Where("email LIKE ? OR CAST(id AS CHAR) = ?", "%"+keyword+"%", keyword) } var total int64 if err := query.Count(&total).Error; err != nil { Fail(c, http.StatusInternalServerError, "failed to count users") return } var users []model.User if err := query.Offset((page - 1) * perPage).Limit(perPage).Find(&users).Error; err != nil { Fail(c, http.StatusInternalServerError, "failed to fetch users") return } userIDs := make([]int, 0, len(users)) for _, user := range users { userIDs = append(userIDs, user.ID) } deviceMap := service.GetUsersDevices(userIDs) realnameStatusByUserID := make(map[int]string, len(userIDs)) if len(userIDs) > 0 { var records []model.RealNameAuth if err := database.DB.Select("user_id", "status").Where("user_id IN ?", userIDs).Find(&records).Error; err == nil { for _, record := range records { realnameStatusByUserID[int(record.UserID)] = record.Status } } } shadowByParentID := make(map[int]model.User, len(userIDs)) if len(userIDs) > 0 { var shadowUsers []model.User if err := database.DB.Select("id", "parent_id", "email").Where("parent_id IN ?", userIDs).Find(&shadowUsers).Error; err == nil { for _, shadow := range shadowUsers { if shadow.ParentID != nil { shadowByParentID[*shadow.ParentID] = shadow } } } } groupNames := loadServerGroupNameMap() items := make([]gin.H, 0, len(users)) for _, user := range users { onlineIP := "" if ips, ok := deviceMap[user.ID]; ok && len(ips) > 0 { onlineIP = strings.Join(ips, ", ") } realnameStatus := realnameStatusByUserID[user.ID] if realnameStatus == "" { realnameStatus = "unverified" } ipv6Shadow, hasIPv6Shadow := shadowByParentID[user.ID] ipv6ShadowID := 0 if hasIPv6Shadow { ipv6ShadowID = ipv6Shadow.ID } items = append(items, gin.H{ "id": user.ID, "email": user.Email, "parent_id": intValue(user.ParentID), "is_shadow_user": user.ParentID != nil, "balance": user.Balance, "uuid": user.UUID, "token": user.Token, "group_id": intValue(user.GroupID), "group_name": groupNames[intFromPointer(user.GroupID)], "group": gin.H{ "id": intValue(user.GroupID), "name": groupNames[intFromPointer(user.GroupID)], }, "plan_id": intValue(user.PlanID), "plan_name": planName(user.Plan), "plan": gin.H{ "id": intValue(user.PlanID), "name": planName(user.Plan), }, "transfer_enable": user.TransferEnable, "u": user.U, "d": user.D, "total_used": user.U + user.D, "banned": user.Banned, "is_admin": user.IsAdmin, "is_staff": user.IsStaff, "device_limit": intValue(user.DeviceLimit), "online_count": intValue(user.OnlineCount), "expired_at": int64Value(user.ExpiredAt), "next_reset_at": int64Value(user.NextResetAt), "last_login_at": int64Value(user.LastLoginAt), "last_login_ip": user.LastLoginIP, "online_ip": onlineIP, "online_ip_count": len(deviceMap[user.ID]), "realname_status": realnameStatus, "realname_label": realNameStatusLabel(realnameStatus), "ipv6_shadow_id": ipv6ShadowID, "ipv6_shadow_email": firstString(ipv6Shadow.Email, service.IPv6ShadowEmail(user.Email)), "ipv6_enabled": hasIPv6Shadow, "created_at": user.CreatedAt, "updated_at": user.UpdatedAt, "remarks": stringValue(user.Remarks), "commission_type": user.CommissionType, "commission_rate": intValue(user.CommissionRate), "commission_balance": user.CommissionBalance, }) } c.JSON(http.StatusOK, gin.H{ "list": items, "data": items, "total": total, "filters": gin.H{ "keyword": keyword, }, "pagination": gin.H{ "current": page, "last_page": calculateLastPage(total, perPage), "per_page": perPage, "total": total, }, }) } func AdminTicketsFetch(c *gin.Context) { params := getFetchParams(c) if ticketID := strings.TrimSpace(params["id"]); ticketID != "" { adminTicketDetail(c, ticketID) return } page := parsePositiveInt(params["page"], 1) perPage := parsePositiveInt(params["per_page"], 50) keyword := strings.TrimSpace(params["keyword"]) query := database.DB.Model(&model.Ticket{}).Order("updated_at DESC, id DESC") if keyword != "" { query = query.Where("subject LIKE ? OR CAST(user_id AS CHAR) = ? OR CAST(id AS CHAR) = ?", "%"+keyword+"%", keyword, keyword) } var total int64 if err := query.Count(&total).Error; err != nil { Fail(c, http.StatusInternalServerError, "failed to count tickets") return } var tickets []model.Ticket if err := query.Offset((page - 1) * perPage).Limit(perPage).Find(&tickets).Error; err != nil { Fail(c, http.StatusInternalServerError, "failed to fetch tickets") return } userEmails := loadUserEmailMap(extractTicketUserIDs(tickets)) messageCounts := loadTicketMessageCountMap(extractTicketIDs(tickets)) items := make([]gin.H, 0, len(tickets)) for _, ticket := range tickets { items = append(items, gin.H{ "id": ticket.ID, "user_id": ticket.UserID, "user_email": userEmails[ticket.UserID], "subject": ticket.Subject, "level": ticket.Level, "status": ticket.Status, "reply_status": ticket.ReplyStatus, "message_count": messageCounts[ticket.ID], "created_at": ticket.CreatedAt, "updated_at": ticket.UpdatedAt, }) } Success(c, gin.H{ "list": items, "data": items, "total": total, "filters": gin.H{ "keyword": keyword, }, "pagination": gin.H{ "current": page, "last_page": calculateLastPage(total, perPage), "per_page": perPage, "total": total, }, }) } func AdminGiftCardStatus(c *gin.Context) { Success(c, gin.H{ "supported": false, "status": "not_integrated", "message": "Gift card management is not fully integrated in the current Go backend yet.", }) } func adminTicketDetail(c *gin.Context, rawID string) { var ticket model.Ticket if err := database.DB.Where("id = ?", rawID).First(&ticket).Error; err != nil { Fail(c, http.StatusNotFound, "ticket not found") return } var messages []model.TicketMessage if err := database.DB.Where("ticket_id = ?", ticket.ID).Order("id ASC").Find(&messages).Error; err != nil { Fail(c, http.StatusInternalServerError, "failed to fetch ticket messages") return } userEmails := loadUserEmailMap([]int{ticket.UserID}) Success(c, gin.H{ "id": ticket.ID, "user_id": ticket.UserID, "user_email": userEmails[ticket.UserID], "subject": ticket.Subject, "level": ticket.Level, "status": ticket.Status, "reply_status": ticket.ReplyStatus, "created_at": ticket.CreatedAt, "updated_at": ticket.UpdatedAt, "messages": buildTicketMessages(messages, 0), }) } func extractOrderUserIDs(orders []model.Order) []int { ids := make([]int, 0, len(orders)) for _, order := range orders { ids = append(ids, order.UserID) } return ids } func extractOrderInviteUserIDs(orders []model.Order) []int { ids := make([]int, 0, len(orders)) for _, order := range orders { if order.InviteUserID != nil { ids = append(ids, *order.InviteUserID) } } return ids } func extractTicketUserIDs(tickets []model.Ticket) []int { ids := make([]int, 0, len(tickets)) for _, ticket := range tickets { ids = append(ids, ticket.UserID) } return ids } func extractTicketIDs(tickets []model.Ticket) []int { ids := make([]int, 0, len(tickets)) for _, ticket := range tickets { ids = append(ids, ticket.ID) } return ids } func intFromPointer(value *int) int { if value == nil { return 0 } return *value } func planName(plan *model.Plan) string { if plan == nil { return "" } return plan.Name }