package admin import ( "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/shopspring/decimal" "go.uber.org/zap" "gorm.io/gorm" "openteam/server/internal/billing" "openteam/server/internal/pkg/httpx" "openteam/server/internal/store" ) type userHandler struct { db *gorm.DB bill *billing.Service log *zap.Logger } // List handles GET /api/v1/admin/users?page&search. func (h *userHandler) List(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) if page < 1 { page = 1 } pageSize := 20 search := c.Query("search") q := h.db.Model(&store.User{}) if search != "" { like := "%" + search + "%" q = q.Where("username LIKE ? OR email LIKE ?", like, like) } var total int64 q.Count(&total) var users []store.User if err := q.Order("id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&users).Error; err != nil { h.log.Warn("list users failed", zap.Error(err)) httpx.Fail(c, http.StatusInternalServerError, "list users failed") return } out := make([]gin.H, 0, len(users)) for i := range users { out = append(out, userDTO(&users[i])) } httpx.OK(c, gin.H{"total": total, "page": page, "items": out}) } // Update handles PATCH /api/v1/admin/users/:id. func (h *userHandler) Update(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { httpx.Fail(c, http.StatusBadRequest, "invalid user id") return } var u store.User if err := h.db.First(&u, id).Error; err != nil { httpx.Fail(c, http.StatusNotFound, "user not found") return } var in struct { Role *string `json:"role"` Status *string `json:"status"` } if !httpx.Bind(c, &in) { return } updates := map[string]any{} if in.Role != nil { if *in.Role != "admin" && *in.Role != "user" { httpx.Fail(c, http.StatusBadRequest, "invalid role") return } updates["role"] = *in.Role } if in.Status != nil { if *in.Status != "active" && *in.Status != "disabled" { httpx.Fail(c, http.StatusBadRequest, "invalid status") return } updates["status"] = *in.Status } if len(updates) > 0 { if err := h.db.Model(&u).Updates(updates).Error; err != nil { h.log.Warn("update user failed", zap.Error(err)) httpx.Fail(c, http.StatusInternalServerError, "update user failed") return } } h.db.First(&u, id) httpx.OK(c, userDTO(&u)) } // AdjustBalance handles POST /api/v1/admin/users/:id/balance. func (h *userHandler) AdjustBalance(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { httpx.Fail(c, http.StatusBadRequest, "invalid user id") return } var in struct { Amount string `json:"amount" binding:"required"` Remark string `json:"remark"` } if !httpx.Bind(c, &in) { return } amount, err := decimal.NewFromString(in.Amount) if err != nil { httpx.Fail(c, http.StatusBadRequest, "invalid amount") return } if amount.IsZero() { httpx.Fail(c, http.StatusBadRequest, "amount must be non-zero") return } after, err := h.bill.AdminAdjust(id, amount.Round(8), in.Remark) if err != nil { if err == billing.ErrInsufficientBalance { httpx.Fail(c, http.StatusBadRequest, "amount would make balance negative") return } h.log.Warn("adjust balance failed", zap.Error(err)) httpx.Fail(c, http.StatusInternalServerError, "adjust balance failed") return } httpx.OK(c, gin.H{"balanceAfter": after.String()}) } func userDTO(u *store.User) gin.H { return gin.H{ "id": u.ID, "username": u.Username, "email": u.Email, "role": u.Role, "balance": u.Balance.String(), "status": u.Status, "lastLoginAt": u.LastLoginAt, "createdAt": u.CreatedAt, } }