refactor(api): split Provider into ProviderGroup and APIKey models

Restructure the provider management system by separating the monolithic
Provider model into two distinct entities:

- ProviderGroup: defines shared upstream configuration (type, base_url,
  google settings, models, status)
- APIKey: represents individual credentials within a group (api_key,
  weight, status, auto_ban, ban settings)

This change also updates:
- Binding model to reference GroupID instead of RouteGroup string
- All CRUD handlers for the new provider-group and api-key endpoints
- Sync service to rebuild provider snapshots from joined tables
- Model registry to aggregate capabilities across group/key pairs
- Access handler to validate namespace existence and subset constraints
- Migration importer to handle the new schema structure
- All related tests to use the new model relationships

BREAKING CHANGE: Provider API endpoints replaced with /provider-groups
and /api-keys endpoints; Binding.RouteGroup replaced with Binding.GroupID
This commit is contained in:
zenfun
2025-12-24 02:15:52 +08:00
parent cd5616dc26
commit dea8363e41
27 changed files with 1222 additions and 1625 deletions

View File

@@ -1,12 +1,14 @@
package api
import (
"fmt"
"net/http"
"strconv"
"strings"
"github.com/ez-api/ez-api/internal/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type AccessResponse struct {
@@ -94,6 +96,10 @@ func (h *Handler) UpdateMasterAccess(c *gin.Context) {
}
nsList := normalizeNamespaces(nextNamespaces, nextDefault)
nextNamespaces = strings.Join(nsList, ",")
if err := ensureNamespacesExist(h.db, nsList); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.db.Model(&m).Updates(map[string]any{
"default_namespace": nextDefault,
@@ -203,6 +209,21 @@ func (h *Handler) UpdateKeyAccess(c *gin.Context) {
}
nsList := normalizeNamespaces(nextNamespaces, nextDefault)
nextNamespaces = strings.Join(nsList, ",")
if err := ensureNamespacesExist(h.db, nsList); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var master model.Master
if err := h.db.First(&master, k.MasterID).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "master not found"})
return
}
masterNamespaces := normalizeNamespaces(master.Namespaces, master.DefaultNamespace)
if !isSubset(nsList, masterNamespaces) {
c.JSON(http.StatusBadRequest, gin.H{"error": "namespaces must be a subset of master namespaces"})
return
}
if err := h.db.Model(&k).Updates(map[string]any{
"default_namespace": nextDefault,
@@ -264,6 +285,39 @@ func normalizeNamespaces(raw string, defaultNamespace string) []string {
return out
}
func ensureNamespacesExist(db *gorm.DB, namespaces []string) error {
if db == nil {
return fmt.Errorf("db required")
}
if len(namespaces) == 0 {
return fmt.Errorf("namespaces required")
}
var rows []model.Namespace
if err := db.Where("name IN ?", namespaces).Find(&rows).Error; err != nil {
return fmt.Errorf("failed to load namespaces")
}
if len(rows) != len(namespaces) {
return fmt.Errorf("namespace not found")
}
return nil
}
func isSubset(child, parent []string) bool {
if len(child) == 0 {
return true
}
parentSet := make(map[string]struct{}, len(parent))
for _, p := range parent {
parentSet[strings.TrimSpace(p)] = struct{}{}
}
for _, c := range child {
if _, ok := parentSet[strings.TrimSpace(c)]; !ok {
return false
}
}
return true
}
func parseUintParam(c *gin.Context, name string) (uint, bool) {
idRaw := strings.TrimSpace(c.Param(name))
if idRaw == "" {