移除被错误提交的测试用exe文件
All checks were successful
build / build (api, amd64, linux) (push) Successful in -49s
build / build (api, arm64, linux) (push) Successful in -47s
build / build (api.exe, amd64, windows) (push) Successful in -47s

修复前后端逻辑
This commit is contained in:
CN-JS-HuiBai
2026-04-17 10:40:05 +08:00
parent 1ed31b9292
commit d077eae2f6
10 changed files with 1794 additions and 1289 deletions

View 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
}