package api import ( "encoding/json" "fmt" "net/http" "strconv" "time" "github.com/gin-gonic/gin" "github.com/openteam/server/internal/pkg/resp" "github.com/openteam/server/internal/store" "gorm.io/gorm" ) // AdminUsers GET /api/v1/admin/users — 用户列表(搜索、分页)。 func (h *Handler) AdminUsers(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) size, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) if page < 1 { page = 1 } if size < 1 || size > 100 { size = 20 } q := h.a.DB.Model(&store.User{}) if kw := c.Query("q"); kw != "" { q = q.Where("username LIKE ? OR email LIKE ?", "%"+kw+"%", "%"+kw+"%") } var total int64 q.Count(&total) var users []store.User q.Order("id DESC").Offset((page - 1) * size).Limit(size).Find(&users) out := make([]gin.H, 0, len(users)) for _, u := range users { out = append(out, h.publicUser(&u)) } resp.OK(c, gin.H{"items": out, "total": total, "page": page, "page_size": size}) } // AdminPatchUser PATCH /api/v1/admin/users/:id — 角色/状态/余额。 func (h *Handler) AdminPatchUser(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { resp.Fail(c, http.StatusBadRequest, "invalid user id") return } var req struct { Role *string `json:"role"` Status *string `json:"status"` } if err := c.ShouldBindJSON(&req); err != nil { resp.Fail(c, http.StatusBadRequest, "invalid input") return } var u store.User if err := h.a.DB.First(&u, id).Error; err != nil { resp.Fail(c, http.StatusNotFound, "user not found") return } updates := map[string]any{} if req.Role != nil { if *req.Role != store.RoleUser && *req.Role != store.RoleAdmin { resp.Fail(c, http.StatusBadRequest, "role must be user or admin") return } updates["role"] = *req.Role } if req.Status != nil { if *req.Status != store.UserStatusActive && *req.Status != store.UserStatusDisabled { resp.Fail(c, http.StatusBadRequest, "status must be active or disabled") return } updates["status"] = *req.Status } if len(updates) > 0 { h.a.DB.Model(&u).Updates(updates) } resp.OK(c, gin.H{"ok": true}) } // AdminAdjustBalance POST /api/v1/admin/users/:id/balance — 调整余额并写流水。 func (h *Handler) AdminAdjustBalance(c *gin.Context) { admin, _ := userFromContext(c) id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { resp.Fail(c, http.StatusBadRequest, "invalid user id") return } var req struct { Change float64 `json:"change" binding:"required"` Remark string `json:"remark"` } if err := c.ShouldBindJSON(&req); err != nil { resp.Fail(c, http.StatusBadRequest, "invalid input: change is required") return } var u store.User if err := h.a.DB.First(&u, id).Error; err != nil { resp.Fail(c, http.StatusNotFound, "user not found") return } newBalance := u.Balance + req.Change ref := fmt.Sprintf("admin:%d:%d", admin.ID, time.Now().UnixNano()) err = h.a.DB.Transaction(func(tx *gorm.DB) error { if err := tx.Model(&store.User{}).Where("id = ?", u.ID).Update("balance", newBalance).Error; err != nil { return err } return tx.Create(&store.BalanceLog{ UserID: u.ID, Change: req.Change, BalanceAfter: newBalance, Type: store.BalanceTypeAdminAdjust, RefID: ref, Remark: req.Remark, }).Error }) if err != nil { resp.Fail(c, http.StatusInternalServerError, "failed to adjust balance") return } resp.OK(c, gin.H{"ok": true, "balance": newBalance}) } // AdminConfig GET /api/v1/admin/config func (h *Handler) AdminConfig(c *gin.Context) { var cfgs []store.SystemConfig h.a.DB.Find(&cfgs) m := map[string]any{} for _, cfg := range cfgs { var v any _ = json.Unmarshal([]byte(cfg.Value), &v) m[cfg.Key] = v } m["registration.mode"] = h.a.Cfg.Auth.RegistrationMode resp.OK(c, gin.H{"config": m}) } // AdminPutConfig PUT /api/v1/admin/config func (h *Handler) AdminPutConfig(c *gin.Context) { var req struct { Config map[string]any `json:"config"` } if err := c.ShouldBindJSON(&req); err != nil { resp.Fail(c, http.StatusBadRequest, "invalid input") return } for k, v := range req.Config { if k == "registration.mode" { if v == "open" || v == "invite" { h.a.Cfg.Auth.RegistrationMode = v.(string) } continue } b, _ := json.Marshal(v) cfg := store.SystemConfig{Key: k, Value: string(b)} h.a.DB.Save(&cfg) } resp.OK(c, gin.H{"ok": true}) }