package api import ( "encoding/json" "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" "gorm.io/gorm/clause" ) // 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 } 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 { resp.OK(c, gin.H{"ok": true}) return } res := h.a.DB.Model(&store.User{}).Where("id = ?", id).Updates(updates) if res.Error != nil { resp.Fail(c, http.StatusInternalServerError, "failed to update user") return } if res.RowsAffected == 0 { resp.Fail(c, http.StatusNotFound, "user not found") return } resp.OK(c, gin.H{"ok": true}) } // AdminAdjustBalance POST /api/v1/admin/users/:id/balance — 手动调余额(写流水)。 func (h *Handler) AdminAdjustBalance(c *gin.Context) { admin := sessionUser(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 { Amount float64 `json:"amount" binding:"required"` Remark string `json:"remark"` } if err := c.ShouldBindJSON(&req); err != nil { resp.Fail(c, http.StatusBadRequest, "invalid input: amount required") return } if req.Amount == 0 { resp.Fail(c, http.StatusBadRequest, "amount must not be zero") return } err = h.a.DB.Transaction(func(tx *gorm.DB) error { var u store.User if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&u, id).Error; err != nil { return err } newBalance := u.Balance + req.Amount if err := tx.Model(&store.User{}).Where("id = ?", id).Update("balance", newBalance).Error; err != nil { return err } ref := "admin:" + strconv.FormatUint(u.ID, 10) + ":" + time.Now().Format("20060102150405") _ = admin.ID // 流水里不冗余管理员 ID;需要时再加 return tx.Create(&store.BalanceLog{ UserID: u.ID, Change: req.Amount, BalanceAfter: newBalance, Type: store.BalanceTypeAdminAdjust, RefID: ref, Remark: req.Remark, }).Error }) if err != nil { resp.Fail(c, http.StatusNotFound, "user not found or failed to adjust") return } resp.OK(c, gin.H{"ok": true}) } // AdminConfig GET /api/v1/admin/config — 全部系统配置。 func (h *Handler) AdminConfig(c *gin.Context) { var cfgs []store.SystemConfig if err := h.a.DB.Find(&cfgs).Error; err != nil { resp.Fail(c, http.StatusInternalServerError, "failed to load config") return } out := gin.H{} for _, cfg := range cfgs { out[cfg.Key] = json.RawMessage(cfg.Value) } resp.OK(c, gin.H{"config": out}) } // AdminPutConfig PUT /api/v1/admin/config — 整表覆盖(upsert)。 func (h *Handler) AdminPutConfig(c *gin.Context) { var req map[string]json.RawMessage if err := c.ShouldBindJSON(&req); err != nil { resp.Fail(c, http.StatusBadRequest, "invalid input") return } err := h.a.DB.Transaction(func(tx *gorm.DB) error { for k, v := range req { cfg := store.SystemConfig{Key: k, Value: string(v)} if err := tx.Save(&cfg).Error; err != nil { return err } } return nil }) if err != nil { resp.Fail(c, http.StatusInternalServerError, "failed to save config") return } resp.OK(c, gin.H{"ok": true}) }