This commit is contained in:
XOF
2025-11-20 12:24:05 +08:00
commit f28bdc751f
164 changed files with 64248 additions and 0 deletions

View File

@@ -0,0 +1,167 @@
// Filename: internal/domain/upstream/handler.go
package upstream
import (
"gemini-balancer/internal/errors"
"gemini-balancer/internal/models"
"gemini-balancer/internal/response"
"net/url"
"strconv"
"github.com/gin-gonic/gin"
)
type Handler struct {
service *Service
}
func NewHandler(service *Service) *Handler {
return &Handler{service: service}
}
// ------ DTOs and Validation ------
type CreateUpstreamRequest struct {
URL string `json:"url" binding:"required"`
Weight int `json:"weight" binding:"omitempty,gte=1,lte=1000"`
Status string `json:"status" binding:"omitempty,oneof=active inactive"`
Description string `json:"description"`
}
type UpdateUpstreamRequest struct {
URL *string `json:"url"`
Weight *int `json:"weight" binding:"omitempty,gte=1,lte=1000"`
Status *string `json:"status" binding:"omitempty,oneof=active inactive"`
Description *string `json:"description"`
}
func isValidURL(rawURL string) bool {
u, err := url.ParseRequestURI(rawURL)
if err != nil {
return false
}
return u.Scheme == "http" || u.Scheme == "https"
}
// --- Handler ---
func (h *Handler) CreateUpstream(c *gin.Context) {
var req CreateUpstreamRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, errors.NewAPIError(errors.ErrInvalidJSON, err.Error()))
return
}
if !isValidURL(req.URL) {
response.Error(c, errors.NewAPIError(errors.ErrValidation, "Invalid URL format"))
return
}
upstream := models.UpstreamEndpoint{
URL: req.URL,
Weight: req.Weight,
Status: req.Status,
Description: req.Description,
}
if err := h.service.Create(&upstream); err != nil {
response.Error(c, errors.ParseDBError(err))
return
}
response.Created(c, upstream)
}
func (h *Handler) ListUpstreams(c *gin.Context) {
upstreams, err := h.service.List()
if err != nil {
response.Error(c, errors.ParseDBError(err))
return
}
response.Success(c, upstreams)
}
func (h *Handler) GetUpstream(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
response.Error(c, errors.NewAPIError(errors.ErrBadRequest, "Invalid ID format"))
return
}
upstream, err := h.service.GetByID(id)
if err != nil {
response.Error(c, errors.ParseDBError(err))
return
}
response.Success(c, upstream)
}
func (h *Handler) UpdateUpstream(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
response.Error(c, errors.NewAPIError(errors.ErrBadRequest, "Invalid ID format"))
return
}
var req UpdateUpstreamRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, errors.NewAPIError(errors.ErrInvalidJSON, err.Error()))
return
}
upstream, err := h.service.GetByID(id)
if err != nil {
response.Error(c, errors.ParseDBError(err))
return
}
if req.URL != nil {
if !isValidURL(*req.URL) {
response.Error(c, errors.NewAPIError(errors.ErrValidation, "Invalid URL format"))
return
}
upstream.URL = *req.URL
}
if req.Weight != nil {
upstream.Weight = *req.Weight
}
if req.Status != nil {
upstream.Status = *req.Status
}
if req.Description != nil {
upstream.Description = *req.Description
}
if err := h.service.Update(upstream); err != nil {
response.Error(c, errors.ParseDBError(err))
return
}
response.Success(c, upstream)
}
func (h *Handler) DeleteUpstream(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
response.Error(c, errors.NewAPIError(errors.ErrBadRequest, "Invalid ID format"))
return
}
rowsAffected, err := h.service.Delete(id)
if err != nil {
response.Error(c, errors.ParseDBError(err))
return
}
if rowsAffected == 0 {
response.Error(c, errors.ErrResourceNotFound)
return
}
response.NoContent(c)
}
// RegisterRoutes
func (h *Handler) RegisterRoutes(rg *gin.RouterGroup) {
upstreamRoutes := rg.Group("/upstreams")
{
upstreamRoutes.POST("/", h.CreateUpstream)
upstreamRoutes.GET("/", h.ListUpstreams)
upstreamRoutes.GET("/:id", h.GetUpstream)
upstreamRoutes.PUT("/:id", h.UpdateUpstream)
upstreamRoutes.DELETE("/:id", h.DeleteUpstream)
}
}

View File

@@ -0,0 +1,36 @@
// Filename: internal/domain/upstream/module.go
package upstream
import (
"gemini-balancer/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type Module struct {
service *Service
handler *Handler
}
func NewModule(db *gorm.DB) *Module {
service := NewService(db)
handler := NewHandler(service)
return &Module{
service: service,
handler: handler,
}
}
// === 领域暴露的公共API ===
// SelectActiveWeighted
func (m *Module) SelectActiveWeighted(upstreams []*models.UpstreamEndpoint) (*models.UpstreamEndpoint, error) {
return m.service.SelectActiveWeighted(upstreams)
}
func (m *Module) RegisterRoutes(router *gin.RouterGroup) {
m.handler.RegisterRoutes(router)
}

View File

@@ -0,0 +1,84 @@
// Filename: internal/domain/upstream/service.go
package upstream
import (
"errors"
"gemini-balancer/internal/models"
"math/rand"
"time"
"gorm.io/gorm"
)
type Service struct {
db *gorm.DB
}
func NewService(db *gorm.DB) *Service {
rand.New(rand.NewSource(time.Now().UnixNano()))
return &Service{db: db}
}
func (s *Service) SelectActiveWeighted(upstreams []*models.UpstreamEndpoint) (*models.UpstreamEndpoint, error) {
activeUpstreams := make([]*models.UpstreamEndpoint, 0)
totalWeight := 0
for _, u := range upstreams {
if u.Status == "active" {
activeUpstreams = append(activeUpstreams, u)
totalWeight += u.Weight
}
}
if len(activeUpstreams) == 0 {
return nil, errors.New("no active upstream endpoints available")
}
if totalWeight <= 0 || len(activeUpstreams) == 1 {
return activeUpstreams[0], nil
}
randomWeight := rand.Intn(totalWeight)
for _, u := range activeUpstreams {
randomWeight -= u.Weight
if randomWeight < 0 {
return u, nil
}
}
return activeUpstreams[len(activeUpstreams)-1], nil
}
// CRUD供Handler调用
func (s *Service) Create(upstream *models.UpstreamEndpoint) error {
if upstream.Weight == 0 {
upstream.Weight = 100 // 默认权重
}
if upstream.Status == "" {
upstream.Status = "active" // 默认状态
}
return s.db.Create(upstream).Error
}
// List Service层只做数据库查询
func (s *Service) List() ([]models.UpstreamEndpoint, error) {
var upstreams []models.UpstreamEndpoint
err := s.db.Find(&upstreams).Error
return upstreams, err
}
// GetByID Service层只做数据库查询
func (s *Service) GetByID(id int) (*models.UpstreamEndpoint, error) {
var upstream models.UpstreamEndpoint
if err := s.db.First(&upstream, id).Error; err != nil {
return nil, err
}
return &upstream, nil
}
// Update Service层只做数据库更新
func (s *Service) Update(upstream *models.UpstreamEndpoint) error {
return s.db.Save(upstream).Error
}
// Delete Service层只做数据库删除
func (s *Service) Delete(id int) (int64, error) {
result := s.db.Delete(&models.UpstreamEndpoint{}, id)
return result.RowsAffected, result.Error
}