Files
openteam/server/internal/admin/users.go
T
SakurasanandClaude Sonnet 5 d0e31b198f feat(server): API relay gateway backend M0-M4
Gin + GORM + pure-Go SQLite. Users/auth (JWT), API key management with
quotas, proxy gateway with weighted channel failover and health checks,
usage/billing ledger, cross-protocol conversion (Anthropic Messages /
OpenAI Chat Completions / OpenAI Responses), and channel/model admin API.
Channels declare native API formats and auto-convert the rest.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 21:05:02 +08:00

141 lines
3.5 KiB
Go

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