feat(server): initialize project skeleton with db and api setup

Establish the foundational structure for the ez-api server.

Key changes include:
- Set up main entry point with graceful shutdown and Gin router
- Configure database connections for PostgreSQL (GORM) and Redis
- Define core data models (User, Provider, Key, Model)
- Implement configuration loading and basic key creation handler
- Add Dockerfile for multi-stage builds and .gitignore
This commit is contained in:
zenfun
2025-12-02 13:35:17 +08:00
commit 58dfe5e9ac
9 changed files with 505 additions and 0 deletions

42
internal/api/handler.go Normal file
View File

@@ -0,0 +1,42 @@
package api
import (
"net/http"
"github.com/ez-api/ez-api/internal/model"
"github.com/ez-api/ez-api/internal/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type Handler struct {
db *gorm.DB
sync *service.SyncService
}
func NewHandler(db *gorm.DB, sync *service.SyncService) *Handler {
return &Handler{db: db, sync: sync}
}
func (h *Handler) CreateKey(c *gin.Context) {
var key model.Key
if err := c.ShouldBindJSON(&key); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Save to DB
if err := h.db.Create(&key).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create key", "details": err.Error()})
return
}
// Sync to Redis
if err := h.sync.SyncKey(&key); err != nil {
// Note: In a real system, we might want to rollback DB or retry async
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync key to Redis", "details": err.Error()})
return
}
c.JSON(http.StatusCreated, key)
}

58
internal/config/config.go Normal file
View File

@@ -0,0 +1,58 @@
package config
import (
"os"
"strconv"
)
type Config struct {
Server ServerConfig
Postgres PostgresConfig
Redis RedisConfig
}
type ServerConfig struct {
Port string
}
type PostgresConfig struct {
DSN string
}
type RedisConfig struct {
Addr string
Password string
DB int
}
func Load() (*Config, error) {
return &Config{
Server: ServerConfig{
Port: getEnv("EZ_API_PORT", "8080"),
},
Postgres: PostgresConfig{
DSN: getEnv("EZ_PG_DSN", "host=localhost user=postgres password=postgres dbname=ezapi port=5432 sslmode=disable"),
},
Redis: RedisConfig{
Addr: getEnv("EZ_REDIS_ADDR", "localhost:6379"),
Password: getEnv("EZ_REDIS_PASSWORD", ""),
DB: getEnvInt("EZ_REDIS_DB", 0),
},
}, nil
}
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
func getEnvInt(key string, fallback int) int {
if value, ok := os.LookupEnv(key); ok {
if i, err := strconv.Atoi(value); err == nil {
return i
}
}
return fallback
}

37
internal/model/models.go Normal file
View File

@@ -0,0 +1,37 @@
package model
import (
"gorm.io/gorm"
)
type User struct {
gorm.Model
Username string `gorm:"uniqueIndex;not null"`
Quota int64 `gorm:"default:0"`
Role string `gorm:"default:'user'"` // admin, user
}
type Provider struct {
gorm.Model
Name string `gorm:"not null"`
Type string `gorm:"not null"` // openai, anthropic, etc.
BaseURL string
APIKey string
}
type Key struct {
gorm.Model
ProviderID *uint
Provider *Provider
KeySecret string `gorm:"not null"`
Balance float64
Status string `gorm:"default:'active'"` // active, suspended
Weight int `gorm:"default:10"`
}
type Model struct {
gorm.Model
Name string `gorm:"uniqueIndex;not null"`
ContextWindow int
CostPerToken float64
}

44
internal/service/sync.go Normal file
View File

@@ -0,0 +1,44 @@
package service
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"github.com/ez-api/ez-api/internal/model"
"github.com/redis/go-redis/v9"
)
type SyncService struct {
rdb *redis.Client
}
func NewSyncService(rdb *redis.Client) *SyncService {
return &SyncService{rdb: rdb}
}
func (s *SyncService) SyncKey(key *model.Key) error {
// Hash the token
hasher := sha256.New()
hasher.Write([]byte(key.KeySecret))
tokenHash := hex.EncodeToString(hasher.Sum(nil))
redisKey := fmt.Sprintf("auth:token:%s", tokenHash)
// Store in Redis
// Using HSet to store multiple fields
fields := map[string]interface{}{
"status": key.Status,
"balance": key.Balance,
}
if key.ProviderID != nil {
fields["master_id"] = *key.ProviderID
} else {
fields["master_id"] = 0 // Default or handle as needed
}
err := s.rdb.HSet(context.Background(), redisKey, fields).Err()
return err
}