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>
141 lines
3.5 KiB
Go
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,
|
|
}
|
|
}
|