移除被错误提交的测试用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
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -771,96 +770,6 @@ func decodeStringSlice(raw *string) []string {
|
||||
return filterNonEmptyStrings(strings.Split(*raw, ","))
|
||||
}
|
||||
|
||||
func marshalJSON(value any, fallbackEmptyArray bool) (*string, error) {
|
||||
if value == nil {
|
||||
if fallbackEmptyArray {
|
||||
empty := "[]"
|
||||
return &empty, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if text, ok := value.(string); ok {
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
if fallbackEmptyArray {
|
||||
empty := "[]"
|
||||
return &empty, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
if json.Valid([]byte(text)) {
|
||||
return &text, nil
|
||||
}
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
text := string(payload)
|
||||
return &text, nil
|
||||
}
|
||||
|
||||
func stringFromAny(value any) string {
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
return typed
|
||||
case json.Number:
|
||||
return typed.String()
|
||||
case float64:
|
||||
return strconv.FormatFloat(typed, 'f', -1, 64)
|
||||
case float32:
|
||||
return strconv.FormatFloat(float64(typed), 'f', -1, 32)
|
||||
case int:
|
||||
return strconv.Itoa(typed)
|
||||
case int64:
|
||||
return strconv.FormatInt(typed, 10)
|
||||
case bool:
|
||||
if typed {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func nullableString(value any) any {
|
||||
text := strings.TrimSpace(stringFromAny(value))
|
||||
if text == "" {
|
||||
return nil
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func intFromAny(value any) int {
|
||||
switch typed := value.(type) {
|
||||
case int:
|
||||
return typed
|
||||
case int8:
|
||||
return int(typed)
|
||||
case int16:
|
||||
return int(typed)
|
||||
case int32:
|
||||
return int(typed)
|
||||
case int64:
|
||||
return int(typed)
|
||||
case float32:
|
||||
return int(typed)
|
||||
case float64:
|
||||
return int(typed)
|
||||
case json.Number:
|
||||
parsed, _ := typed.Int64()
|
||||
return int(parsed)
|
||||
case string:
|
||||
parsed, _ := strconv.Atoi(strings.TrimSpace(typed))
|
||||
return parsed
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func float32FromAny(value any) (float32, bool) {
|
||||
switch typed := value.(type) {
|
||||
case float32:
|
||||
@@ -891,64 +800,19 @@ func nullableInt(value any) any {
|
||||
}
|
||||
|
||||
func nullableInt64(value any) any {
|
||||
switch typed := value.(type) {
|
||||
case int64:
|
||||
if typed > 0 {
|
||||
return typed
|
||||
}
|
||||
case int:
|
||||
if typed > 0 {
|
||||
return int64(typed)
|
||||
}
|
||||
case float64:
|
||||
if typed > 0 {
|
||||
return int64(typed)
|
||||
}
|
||||
case string:
|
||||
if parsed, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64); err == nil && parsed > 0 {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func boolFromAny(value any) bool {
|
||||
switch typed := value.(type) {
|
||||
case bool:
|
||||
return typed
|
||||
case int:
|
||||
return typed != 0
|
||||
case int64:
|
||||
return typed != 0
|
||||
case float64:
|
||||
return typed != 0
|
||||
case string:
|
||||
text := strings.TrimSpace(strings.ToLower(typed))
|
||||
return text == "1" || text == "true" || text == "yes" || text == "on"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func stringValue(value *string) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func intValue(value *int) any {
|
||||
if value == nil {
|
||||
parsed := intFromAny(value)
|
||||
if parsed <= 0 {
|
||||
return nil
|
||||
}
|
||||
return *value
|
||||
return int64(parsed)
|
||||
}
|
||||
|
||||
func int64Value(value *int64) any {
|
||||
if value == nil {
|
||||
func nullableString(value any) any {
|
||||
text := strings.TrimSpace(stringFromAny(value))
|
||||
if text == "" {
|
||||
return nil
|
||||
}
|
||||
return *value
|
||||
return text
|
||||
}
|
||||
|
||||
func filterNonEmptyStrings(values any) []string {
|
||||
@@ -972,23 +836,6 @@ func filterNonEmptyStrings(values any) []string {
|
||||
return result
|
||||
}
|
||||
|
||||
func sanitizePositiveIDs(ids []int) []int {
|
||||
unique := make(map[int]struct{}, len(ids))
|
||||
result := make([]int, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if id <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, exists := unique[id]; exists {
|
||||
continue
|
||||
}
|
||||
unique[id] = struct{}{}
|
||||
result = append(result, id)
|
||||
}
|
||||
sort.Ints(result)
|
||||
return result
|
||||
}
|
||||
|
||||
func intSliceContains(values []int, target int) bool {
|
||||
for _, value := range values {
|
||||
if value == target {
|
||||
@@ -1007,9 +854,3 @@ func isAllowedRouteAction(action string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func unixTimeValue(value *time.Time) any {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return value.Unix()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"xboard-go/internal/database"
|
||||
"xboard-go/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -29,3 +36,201 @@ func NotImplemented(endpoint string) gin.HandlerFunc {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func intFromAny(value any) int {
|
||||
switch typed := value.(type) {
|
||||
case int:
|
||||
return typed
|
||||
case int8:
|
||||
return int(typed)
|
||||
case int16:
|
||||
return int(typed)
|
||||
case int32:
|
||||
return int(typed)
|
||||
case int64:
|
||||
return int(typed)
|
||||
case float32:
|
||||
return int(typed)
|
||||
case float64:
|
||||
return int(typed)
|
||||
case json.Number:
|
||||
parsed, _ := typed.Int64()
|
||||
return int(parsed)
|
||||
case string:
|
||||
parsed, _ := strconv.Atoi(strings.TrimSpace(typed))
|
||||
return parsed
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func stringFromAny(value any) string {
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
return typed
|
||||
case json.Number:
|
||||
return typed.String()
|
||||
case float64:
|
||||
return strconv.FormatFloat(typed, 'f', -1, 64)
|
||||
case float32:
|
||||
return strconv.FormatFloat(float64(typed), 'f', -1, 32)
|
||||
case int:
|
||||
return strconv.Itoa(typed)
|
||||
case int64:
|
||||
return strconv.FormatInt(typed, 10)
|
||||
case bool:
|
||||
if typed {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func boolFromAny(value any) bool {
|
||||
switch typed := value.(type) {
|
||||
case bool:
|
||||
return typed
|
||||
case int:
|
||||
return typed != 0
|
||||
case int64:
|
||||
return typed != 0
|
||||
case float64:
|
||||
return typed != 0
|
||||
case string:
|
||||
text := strings.TrimSpace(strings.ToLower(typed))
|
||||
return text == "1" || text == "true" || text == "yes" || text == "on"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func intValue(value *int) any {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func int64Value(value *int64) any {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func stringValue(value *string) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func unixTimeValue(value *time.Time) any {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return value.Unix()
|
||||
}
|
||||
|
||||
func marshalJSON(value any, fallbackEmptyArray bool) (*string, error) {
|
||||
if value == nil {
|
||||
if fallbackEmptyArray {
|
||||
empty := "[]"
|
||||
return &empty, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if text, ok := value.(string); ok {
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
if fallbackEmptyArray {
|
||||
empty := "[]"
|
||||
return &empty, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
if json.Valid([]byte(text)) {
|
||||
return &text, nil
|
||||
}
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
text := string(payload)
|
||||
return &text, nil
|
||||
}
|
||||
|
||||
func sanitizePositiveIDs(ids []int) []int {
|
||||
unique := make(map[int]struct{}, len(ids))
|
||||
result := make([]int, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if id <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, exists := unique[id]; exists {
|
||||
continue
|
||||
}
|
||||
unique[id] = struct{}{}
|
||||
result = append(result, id)
|
||||
}
|
||||
sort.Ints(result)
|
||||
return result
|
||||
}
|
||||
|
||||
func loadServerGroupNameMap() map[int]string {
|
||||
var groups []model.ServerGroup
|
||||
_ = database.DB.Find(&groups).Error
|
||||
|
||||
result := make(map[int]string, len(groups))
|
||||
for _, group := range groups {
|
||||
result[group.ID] = group.Name
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func loadUserEmailMap(userIDs []int) map[int]string {
|
||||
result := make(map[int]string)
|
||||
if len(userIDs) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
var users []model.User
|
||||
if err := database.DB.Select("id", "email").Where("id IN ?", sanitizePositiveIDs(userIDs)).Find(&users).Error; err != nil {
|
||||
return result
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
result[user.ID] = user.Email
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func loadTicketMessageCountMap(ticketIDs []int) map[int]int64 {
|
||||
result := make(map[int]int64)
|
||||
if len(ticketIDs) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
type ticketCount struct {
|
||||
TicketID int
|
||||
Total int64
|
||||
}
|
||||
var counts []ticketCount
|
||||
if err := database.DB.Model(&model.TicketMessage{}).
|
||||
Select("ticket_id, COUNT(*) AS total").
|
||||
Where("ticket_id IN ?", sanitizePositiveIDs(ticketIDs)).
|
||||
Group("ticket_id").
|
||||
Scan(&counts).Error; err != nil {
|
||||
return result
|
||||
}
|
||||
|
||||
for _, item := range counts {
|
||||
result[item.TicketID] = item.Total
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -12,10 +12,6 @@ import (
|
||||
)
|
||||
|
||||
func PluginUserOnlineDevicesUsers(c *gin.Context) {
|
||||
if !service.IsPluginEnabled(service.PluginUserOnlineDevices) {
|
||||
Fail(c, 400, "plugin is not enabled")
|
||||
return
|
||||
}
|
||||
|
||||
page := parsePositiveInt(c.DefaultQuery("page", "1"), 1)
|
||||
perPage := parsePositiveInt(c.DefaultQuery("per_page", "20"), 20)
|
||||
@@ -92,10 +88,6 @@ func PluginUserOnlineDevicesUsers(c *gin.Context) {
|
||||
}
|
||||
|
||||
func PluginUserOnlineDevicesGetIP(c *gin.Context) {
|
||||
if !service.IsPluginEnabled(service.PluginUserOnlineDevices) {
|
||||
Fail(c, 400, "plugin is not enabled")
|
||||
return
|
||||
}
|
||||
|
||||
user, ok := currentUser(c)
|
||||
if !ok {
|
||||
@@ -138,10 +130,6 @@ func PluginUserOnlineDevicesGetIP(c *gin.Context) {
|
||||
}
|
||||
|
||||
func PluginUserAddIPv6Check(c *gin.Context) {
|
||||
if !service.IsPluginEnabled(service.PluginUserAddIPv6) {
|
||||
Fail(c, 400, "plugin is not enabled")
|
||||
return
|
||||
}
|
||||
|
||||
user, ok := currentUser(c)
|
||||
if !ok {
|
||||
@@ -171,10 +159,6 @@ func PluginUserAddIPv6Check(c *gin.Context) {
|
||||
}
|
||||
|
||||
func PluginUserAddIPv6Enable(c *gin.Context) {
|
||||
if !service.IsPluginEnabled(service.PluginUserAddIPv6) {
|
||||
Fail(c, 400, "plugin is not enabled")
|
||||
return
|
||||
}
|
||||
|
||||
user, ok := currentUser(c)
|
||||
if !ok {
|
||||
@@ -191,10 +175,6 @@ func PluginUserAddIPv6Enable(c *gin.Context) {
|
||||
}
|
||||
|
||||
func PluginUserAddIPv6SyncPassword(c *gin.Context) {
|
||||
if !service.IsPluginEnabled(service.PluginUserAddIPv6) {
|
||||
Fail(c, 400, "plugin is not enabled")
|
||||
return
|
||||
}
|
||||
|
||||
user, ok := currentUser(c)
|
||||
if !ok {
|
||||
@@ -234,41 +214,6 @@ func AdminSystemStatus(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func AdminPluginsList(c *gin.Context) {
|
||||
var plugins []model.Plugin
|
||||
if err := database.DB.Order("id ASC").Find(&plugins).Error; err != nil {
|
||||
Fail(c, 500, "failed to fetch plugins")
|
||||
return
|
||||
}
|
||||
Success(c, plugins)
|
||||
}
|
||||
|
||||
func AdminPluginTypes(c *gin.Context) {
|
||||
Success(c, []string{"feature", "payment"})
|
||||
}
|
||||
|
||||
func AdminPluginIntegrationStatus(c *gin.Context) {
|
||||
Success(c, gin.H{
|
||||
"user_online_devices": gin.H{
|
||||
"enabled": service.IsPluginEnabled(service.PluginUserOnlineDevices),
|
||||
"status": "complete",
|
||||
"summary": "用户侧在线设备概览与后台设备监控已接入 Go 后端。",
|
||||
"endpoints": []string{"/api/v1/user/user-online-devices/get-ip", "/api/v1/" + service.GetAdminSecurePath() + "/user-online-devices/users"},
|
||||
},
|
||||
"real_name_verification": gin.H{
|
||||
"enabled": service.IsPluginEnabled(service.PluginRealNameVerification),
|
||||
"status": "complete",
|
||||
"summary": "实名状态查询、提交、后台审核与批量同步均已整合,并补齐 auto_approve/allow_resubmit_after_reject 行为。",
|
||||
"endpoints": []string{"/api/v1/user/real-name-verification/status", "/api/v1/user/real-name-verification/submit", "/api/v1/" + service.GetAdminSecurePath() + "/realname/records"},
|
||||
},
|
||||
"user_add_ipv6_subscription": gin.H{
|
||||
"enabled": service.IsPluginEnabled(service.PluginUserAddIPv6),
|
||||
"status": "integrated_with_runtime_sync",
|
||||
"summary": "用户启用、密码同步与运行时影子账号同步已接入;订单生命周期自动钩子仍依赖后续完整订单流重构。",
|
||||
"endpoints": []string{"/api/v1/user/user-add-ipv6-subscription/check", "/api/v1/user/user-add-ipv6-subscription/enable", "/api/v1/user/user-add-ipv6-subscription/sync-password"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func parsePositiveInt(raw string, defaultValue int) int {
|
||||
value, err := strconv.Atoi(strings.TrimSpace(raw))
|
||||
|
||||
@@ -14,11 +14,6 @@ import (
|
||||
const unverifiedExpiration = int64(946684800)
|
||||
|
||||
func PluginRealNameStatus(c *gin.Context) {
|
||||
if !service.IsPluginEnabled(service.PluginRealNameVerification) {
|
||||
Fail(c, 400, "plugin is not enabled")
|
||||
return
|
||||
}
|
||||
|
||||
user, ok := currentUser(c)
|
||||
if !ok {
|
||||
Fail(c, 401, "unauthorized")
|
||||
@@ -54,11 +49,6 @@ func PluginRealNameStatus(c *gin.Context) {
|
||||
}
|
||||
|
||||
func PluginRealNameSubmit(c *gin.Context) {
|
||||
if !service.IsPluginEnabled(service.PluginRealNameVerification) {
|
||||
Fail(c, 400, "plugin is not enabled")
|
||||
return
|
||||
}
|
||||
|
||||
user, ok := currentUser(c)
|
||||
if !ok {
|
||||
Fail(c, 401, "unauthorized")
|
||||
@@ -126,11 +116,6 @@ func PluginRealNameSubmit(c *gin.Context) {
|
||||
}
|
||||
|
||||
func PluginRealNameRecords(c *gin.Context) {
|
||||
if !service.IsPluginEnabled(service.PluginRealNameVerification) {
|
||||
Fail(c, 400, "plugin is not enabled")
|
||||
return
|
||||
}
|
||||
|
||||
page := parsePositiveInt(c.DefaultQuery("page", "1"), 1)
|
||||
perPage := parsePositiveInt(c.DefaultQuery("per_page", "20"), 20)
|
||||
keyword := strings.TrimSpace(c.Query("keyword"))
|
||||
@@ -192,11 +177,6 @@ func PluginRealNameRecords(c *gin.Context) {
|
||||
}
|
||||
|
||||
func PluginRealNameReview(c *gin.Context) {
|
||||
if !service.IsPluginEnabled(service.PluginRealNameVerification) {
|
||||
Fail(c, 400, "plugin is not enabled")
|
||||
return
|
||||
}
|
||||
|
||||
userID := parsePositiveInt(c.Param("userId"), 0)
|
||||
if userID == 0 {
|
||||
Fail(c, 400, "invalid user id")
|
||||
@@ -239,11 +219,6 @@ func PluginRealNameReview(c *gin.Context) {
|
||||
}
|
||||
|
||||
func PluginRealNameReset(c *gin.Context) {
|
||||
if !service.IsPluginEnabled(service.PluginRealNameVerification) {
|
||||
Fail(c, 400, "plugin is not enabled")
|
||||
return
|
||||
}
|
||||
|
||||
userID := parsePositiveInt(c.Param("userId"), 0)
|
||||
if userID == 0 {
|
||||
Fail(c, 400, "invalid user id")
|
||||
@@ -256,18 +231,10 @@ func PluginRealNameReset(c *gin.Context) {
|
||||
}
|
||||
|
||||
func PluginRealNameSyncAll(c *gin.Context) {
|
||||
if !service.IsPluginEnabled(service.PluginRealNameVerification) {
|
||||
Fail(c, 400, "plugin is not enabled")
|
||||
return
|
||||
}
|
||||
SuccessMessage(c, "sync completed", performGlobalRealNameSync())
|
||||
}
|
||||
|
||||
func PluginRealNameApproveAll(c *gin.Context) {
|
||||
if !service.IsPluginEnabled(service.PluginRealNameVerification) {
|
||||
Fail(c, 400, "plugin is not enabled")
|
||||
return
|
||||
}
|
||||
|
||||
var users []model.User
|
||||
database.DB.Where("email NOT LIKE ?", "%-ipv6@%").Find(&users)
|
||||
|
||||
@@ -63,12 +63,19 @@ func AdminAppPage(c *gin.Context) {
|
||||
"title": service.MustGetString("app_name", "XBoard") + " Admin",
|
||||
"securePath": securePath,
|
||||
"api": map[string]string{
|
||||
"adminConfig": "/api/v2/" + securePath + "/config/fetch",
|
||||
"systemStatus": "/api/v2/" + securePath + "/system/getSystemStatus",
|
||||
"plugins": "/api/v2/" + securePath + "/plugin/getPlugins",
|
||||
"integration": "/api/v2/" + securePath + "/plugin/integration-status",
|
||||
"realnameBase": "/api/v1/" + securePath + "/realname",
|
||||
"onlineDevices": "/api/v1/" + securePath + "/user-online-devices/users",
|
||||
"adminConfig": "/api/v2/" + securePath + "/config/fetch",
|
||||
"saveConfig": "/api/v2/" + securePath + "/config/save",
|
||||
"dashboardSummary": "/api/v2/" + securePath + "/dashboard/summary",
|
||||
"systemStatus": "/api/v2/" + securePath + "/system/getSystemStatus",
|
||||
"serverNodeSort": "/api/v2/" + securePath + "/server/manage/sort",
|
||||
"plans": "/api/v2/" + securePath + "/plan/fetch",
|
||||
"orders": "/api/v2/" + securePath + "/order/fetch",
|
||||
"coupons": "/api/v2/" + securePath + "/coupon/fetch",
|
||||
"users": "/api/v2/" + securePath + "/user/fetch",
|
||||
"tickets": "/api/v2/" + securePath + "/ticket/fetch",
|
||||
"giftCardStatus": "/api/v2/" + securePath + "/gift-card/status",
|
||||
"realnameBase": "/api/v2/" + securePath + "/realname",
|
||||
"onlineDevices": "/api/v2/" + securePath + "/user-online-devices/users",
|
||||
},
|
||||
}
|
||||
payload := adminAppViewData{
|
||||
|
||||
Reference in New Issue
Block a user