移除被错误提交的测试用exe文件
修复前后端逻辑
This commit is contained in:
714
internal/handler/admin_resource_api.go
Normal file
714
internal/handler/admin_resource_api.go
Normal file
@@ -0,0 +1,714 @@
|
||||
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()
|
||||
monthAgo := now - 30*24*60*60
|
||||
|
||||
var totalUsers int64
|
||||
var newUsers int64
|
||||
var activeUsers int64
|
||||
var totalOrders int64
|
||||
var pendingOrders int64
|
||||
var pendingTickets int64
|
||||
var totalServers int64
|
||||
var totalPlans int64
|
||||
var totalCoupons int64
|
||||
var totalGroups int64
|
||||
var totalRoutes int64
|
||||
var onlineUsers int64
|
||||
var paidOrderAmount int64
|
||||
|
||||
database.DB.Model(&model.User{}).Count(&totalUsers)
|
||||
database.DB.Model(&model.User{}).Where("created_at >= ?", monthAgo).Count(&newUsers)
|
||||
database.DB.Model(&model.User{}).Where("last_login_at IS NOT NULL AND last_login_at >= ?", monthAgo).Count(&activeUsers)
|
||||
database.DB.Model(&model.User{}).Where("online_count IS NOT NULL AND online_count > 0").Count(&onlineUsers)
|
||||
database.DB.Model(&model.Order{}).Count(&totalOrders)
|
||||
database.DB.Model(&model.Order{}).Where("status = ?", 0).Count(&pendingOrders)
|
||||
database.DB.Model(&model.Order{}).Where("status NOT IN ?", []int{0, 2}).Select("COALESCE(SUM(total_amount), 0)").Scan(&paidOrderAmount)
|
||||
database.DB.Model(&model.Ticket{}).Where("status = ?", 0).Count(&pendingTickets)
|
||||
database.DB.Model(&model.Server{}).Count(&totalServers)
|
||||
database.DB.Model(&model.Plan{}).Count(&totalPlans)
|
||||
database.DB.Model(&model.Coupon{}).Count(&totalCoupons)
|
||||
database.DB.Model(&model.ServerGroup{}).Count(&totalGroups)
|
||||
database.DB.Model(&model.ServerRoute{}).Count(&totalRoutes)
|
||||
|
||||
Success(c, gin.H{
|
||||
"server_time": now,
|
||||
"total_users": totalUsers,
|
||||
"new_users_30d": newUsers,
|
||||
"active_users_30d": activeUsers,
|
||||
"online_users": onlineUsers,
|
||||
"total_orders": totalOrders,
|
||||
"pending_orders": pendingOrders,
|
||||
"paid_order_amount": paidOrderAmount,
|
||||
"pending_tickets": pendingTickets,
|
||||
"total_servers": totalServers,
|
||||
"total_plans": totalPlans,
|
||||
"total_coupons": totalCoupons,
|
||||
"total_groups": totalGroups,
|
||||
"total_routes": totalRoutes,
|
||||
"secure_path": service.GetAdminSecurePath(),
|
||||
"app_name": service.MustGetString("app_name", "XBoard"),
|
||||
"app_url": service.GetAppURL(),
|
||||
"server_pull_period": service.MustGetInt("server_pull_interval", 60),
|
||||
"server_push_period": service.MustGetInt("server_push_interval", 60),
|
||||
})
|
||||
}
|
||||
|
||||
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) {
|
||||
page := parsePositiveInt(c.DefaultQuery("page", "1"), 1)
|
||||
perPage := parsePositiveInt(c.DefaultQuery("per_page", "50"), 50)
|
||||
keyword := strings.TrimSpace(c.Query("keyword"))
|
||||
statusFilter := strings.TrimSpace(c.Query("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))
|
||||
items := make([]gin.H, 0, len(orders))
|
||||
for _, order := range orders {
|
||||
item := normalizeOrder(order)
|
||||
item["user_email"] = userEmails[order.UserID]
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
Success(c, gin.H{
|
||||
"list": items,
|
||||
"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
|
||||
}
|
||||
|
||||
// Update user
|
||||
var user model.User
|
||||
if err := tx.Where("id = ?", order.ID).First(&user).Error; err == nil {
|
||||
// Calculate expiration and traffic
|
||||
// Simplified logic: set plan and transfer_enable
|
||||
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
|
||||
}
|
||||
Success(c, true)
|
||||
}
|
||||
|
||||
func AdminUsersFetch(c *gin.Context) {
|
||||
page := parsePositiveInt(c.DefaultQuery("page", "1"), 1)
|
||||
perPage := parsePositiveInt(c.DefaultQuery("per_page", "50"), 50)
|
||||
keyword := strings.TrimSpace(c.Query("keyword"))
|
||||
|
||||
query := database.DB.Model(&model.User{}).Preload("Plan").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
|
||||
}
|
||||
|
||||
groupNames := loadServerGroupNameMap()
|
||||
items := make([]gin.H, 0, len(users))
|
||||
for _, user := range users {
|
||||
items = append(items, gin.H{
|
||||
"id": user.ID,
|
||||
"email": user.Email,
|
||||
"balance": user.Balance,
|
||||
"group_id": intValue(user.GroupID),
|
||||
"group_name": groupNames[intFromPointer(user.GroupID)],
|
||||
"plan_id": intValue(user.PlanID),
|
||||
"plan_name": planName(user.Plan),
|
||||
"transfer_enable": user.TransferEnable,
|
||||
"u": user.U,
|
||||
"d": 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),
|
||||
"last_login_at": int64Value(user.LastLoginAt),
|
||||
"last_online_at": unixTimeValue(user.LastOnlineAt),
|
||||
"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,
|
||||
})
|
||||
}
|
||||
|
||||
Success(c, gin.H{
|
||||
"list": items,
|
||||
"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) {
|
||||
if ticketID := strings.TrimSpace(c.Query("id")); ticketID != "" {
|
||||
adminTicketDetail(c, ticketID)
|
||||
return
|
||||
}
|
||||
|
||||
page := parsePositiveInt(c.DefaultQuery("page", "1"), 1)
|
||||
perPage := parsePositiveInt(c.DefaultQuery("per_page", "50"), 50)
|
||||
keyword := strings.TrimSpace(c.Query("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,
|
||||
"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 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
|
||||
}
|
||||
Reference in New Issue
Block a user