refact:openteam

This commit is contained in:
Sakurasan
2025-01-01 23:10:01 +08:00
parent 1f5e1c221c
commit 65d6d12972
18 changed files with 1126 additions and 106 deletions

17
team/consts/consts.go Normal file
View File

@@ -0,0 +1,17 @@
package consts
const (
RoleGuest = iota * 10
RoleUser
RoleAdmin
RoleSuperAdmin
)
const (
StatusEnabled = iota
StatusDisabled
StatusExpired // 过期
StatusExhausted // 耗尽
StatusDeleted
)
const UnlimitedQuota = -1

145
team/dao/apikey.go Normal file
View File

@@ -0,0 +1,145 @@
package dao
import (
"errors"
"opencatd-open/team/model"
"time"
"gorm.io/gorm"
)
var _ ApiKeyRepository = (*ApiKeyDAO)(nil)
type ApiKeyRepository interface {
Create(apiKey *model.ApiKey) error
GetByID(id int64) (*model.ApiKey, error)
GetByName(name string) (*model.ApiKey, error)
GetByApiKey(apiKeyValue string) (*model.ApiKey, error)
Update(apiKey *model.ApiKey) error
Delete(id int64) error
List(offset, limit int, status *int) ([]model.ApiKey, error)
Enable(id int64) error
Disable(id int64) error
BatchEnable(ids []int64) error
BatchDisable(ids []int64) error
BatchDelete(ids []int64) error
Count() (int64, error)
}
type ApiKeyDAO struct {
db *gorm.DB
}
func NewApiKeyDAO(db *gorm.DB) *ApiKeyDAO {
return &ApiKeyDAO{db: db}
}
// CreateApiKey 创建ApiKey
func (dao *ApiKeyDAO) Create(apiKey *model.ApiKey) error {
if apiKey == nil {
return errors.New("apiKey is nil")
}
return dao.db.Create(apiKey).Error
}
// GetApiKeyByID 根据ID获取ApiKey
func (dao *ApiKeyDAO) GetByID(id int64) (*model.ApiKey, error) {
var apiKey model.ApiKey
err := dao.db.First(&apiKey, id).Error
if err != nil {
return nil, err
}
return &apiKey, nil
}
// GetApiKeyByName 根据名称获取ApiKey
func (dao *ApiKeyDAO) GetByName(name string) (*model.ApiKey, error) {
var apiKey model.ApiKey
err := dao.db.Where("name = ?", name).First(&apiKey).Error
if err != nil {
return nil, err
}
return &apiKey, nil
}
// GetApiKeyByApiKey 根据ApiKey值获取ApiKey
func (dao *ApiKeyDAO) GetByApiKey(apiKeyValue string) (*model.ApiKey, error) {
var apiKey model.ApiKey
err := dao.db.Where("api_key = ?", apiKeyValue).First(&apiKey).Error
if err != nil {
return nil, err
}
return &apiKey, nil
}
// UpdateApiKey 更新ApiKey信息
func (dao *ApiKeyDAO) Update(apiKey *model.ApiKey) error {
if apiKey == nil {
return errors.New("apiKey is nil")
}
apiKey.UpdatedAt = time.Now()
return dao.db.Save(apiKey).Error
}
// DeleteApiKey 删除ApiKey
func (dao *ApiKeyDAO) Delete(id int64) error {
return dao.db.Delete(&model.ApiKey{}, id).Error
}
// ListApiKeys 获取ApiKey列表
func (dao *ApiKeyDAO) List(offset, limit int, status *int) ([]model.ApiKey, error) {
var apiKeys []model.ApiKey
db := dao.db.Offset(offset).Limit(limit)
if status != nil {
db = db.Where("status = ?", *status)
}
err := db.Find(&apiKeys).Error
if err != nil {
return nil, err
}
return apiKeys, nil
}
// EnableApiKey 启用ApiKey
func (dao *ApiKeyDAO) Enable(id int64) error {
return dao.db.Model(&model.ApiKey{}).Where("id = ?", id).Update("status", 0).Error
}
// DisableApiKey 禁用ApiKey
func (dao *ApiKeyDAO) Disable(id int64) error {
return dao.db.Model(&model.ApiKey{}).Where("id = ?", id).Update("status", 1).Error
}
// BatchEnableApiKeys 批量启用ApiKey
func (dao *ApiKeyDAO) BatchEnable(ids []int64) error {
if len(ids) == 0 {
return errors.New("ids is empty")
}
return dao.db.Model(&model.ApiKey{}).Where("id IN ?", ids).Update("status", 0).Error
}
// BatchDisableApiKeys 批量禁用ApiKey
func (dao *ApiKeyDAO) BatchDisable(ids []int64) error {
if len(ids) == 0 {
return errors.New("ids is empty")
}
return dao.db.Model(&model.ApiKey{}).Where("id IN ?", ids).Update("status", 1).Error
}
// BatchDeleteApiKey 批量删除ApiKey
func (dao *ApiKeyDAO) BatchDelete(ids []int64) error {
if len(ids) == 0 {
return errors.New("ids is empty")
}
return dao.db.Delete(&model.ApiKey{}, ids).Error
}
// CountApiKeys 获取ApiKey总数
func (dao *ApiKeyDAO) Count() (int64, error) {
var count int64
err := dao.db.Model(&model.ApiKey{}).Count(&count).Error
if err != nil {
return 0, err
}
return count, nil
}

120
team/dao/token.go Normal file
View File

@@ -0,0 +1,120 @@
package dao
import (
"errors"
"opencatd-open/team/model"
"gorm.io/gorm"
)
// 确保 TokenDAO 实现了 TokenDAOInterface 接口
var _ TokenDAOInterface = (*TokenDAO)(nil)
type TokenDAOInterface interface {
Create(token *model.Token) error
GetByID(id int) (*model.Token, error)
GetByKey(key string) (*model.Token, error)
GetByUserID(userID int) ([]model.Token, error)
Update(token *model.Token) error
Delete(id int) error
List(offset, limit int) ([]model.Token, error)
Disable(id int) error
Enable(id int) error
BatchDisable(ids []int) error
BatchEnable(ids []int) error
BatchDelete(ids []int) error
}
type TokenDAO struct {
db *gorm.DB
}
func NewTokenDAO(db *gorm.DB) *TokenDAO {
return &TokenDAO{db: db}
}
// CreateToken 创建 Token
func (dao *TokenDAO) Create(token *model.Token) error {
if token == nil {
return errors.New("token is nil")
}
return dao.db.Create(token).Error
}
// GetTokenByID 根据 ID 获取 Token
func (dao *TokenDAO) GetByID(id int) (*model.Token, error) {
var token model.Token
err := dao.db.First(&token, id).Error
if err != nil {
return nil, err
}
return &token, nil
}
// GetTokenByKey 根据 Key 获取 Token
func (dao *TokenDAO) GetByKey(key string) (*model.Token, error) {
var token model.Token
err := dao.db.Where("key = ?", key).First(&token).Error
if err != nil {
return nil, err
}
return &token, nil
}
// GetTokensByUserID 根据 UserID 获取 Token 列表
func (dao *TokenDAO) GetByUserID(userID int) ([]model.Token, error) {
var tokens []model.Token
err := dao.db.Where("user_id = ?", userID).Find(&tokens).Error
if err != nil {
return nil, err
}
return tokens, nil
}
// UpdateToken 更新 Token 信息
func (dao *TokenDAO) Update(token *model.Token) error {
if token == nil {
return errors.New("token is nil")
}
return dao.db.Save(token).Error
}
// DeleteToken 删除 Token
func (dao *TokenDAO) Delete(id int) error {
return dao.db.Delete(&model.Token{}, id).Error
}
// ListTokens 获取 Token 列表
func (dao *TokenDAO) List(offset, limit int) ([]model.Token, error) {
var tokens []model.Token
err := dao.db.Offset(offset).Limit(limit).Find(&tokens).Error
if err != nil {
return nil, err
}
return tokens, nil
}
// DisableToken 禁用 Token
func (dao *TokenDAO) Disable(id int) error {
return dao.db.Model(&model.Token{}).Where("id = ?", id).Update("status", false).Error
}
// EnableToken 启用 Token
func (dao *TokenDAO) Enable(id int) error {
return dao.db.Model(&model.Token{}).Where("id = ?", id).Update("status", true).Error
}
// BatchDisableTokens 批量禁用 Token
func (dao *TokenDAO) BatchDisable(ids []int) error {
return dao.db.Model(&model.Token{}).Where("id IN ?", ids).Update("status", false).Error
}
// BatchEnableTokens 批量启用 Token
func (dao *TokenDAO) BatchEnable(ids []int) error {
return dao.db.Model(&model.Token{}).Where("id IN ?", ids).Update("status", true).Error
}
// BatchDeleteTokens 批量删除 Token
func (dao *TokenDAO) BatchDelete(ids []int) error {
return dao.db.Where("id IN ?", ids).Delete(&model.Token{}).Error
}

123
team/dao/user.go Normal file
View File

@@ -0,0 +1,123 @@
package dao
import (
"errors"
"opencatd-open/team/model"
"time"
"gorm.io/gorm"
)
// 确保 UserDAO 实现了 UserRepository 接口
var _ UserRepository = (*UserDAO)(nil)
// UserRepository 定义用户数据访问操作的接口
type UserRepository interface {
Create(user *model.User) error
GetByID(id int64) (*model.User, error)
GetByUsername(username string) (*model.User, error)
Update(user *model.User) error
Delete(id int64) error
List(offset, limit int) ([]model.User, error)
Enable(id int64) error
Disable(id int64) error
BatchEnable(ids []int64) error
BatchDisable(ids []int64) error
BatchDelete(ids []int64) error
}
type UserDAO struct {
db *gorm.DB
}
func NewUserDAO(db *gorm.DB) *UserDAO {
return &UserDAO{db: db}
}
// CreateUser 创建用户
func (dao *UserDAO) Create(user *model.User) error {
if user == nil {
return errors.New("user is nil")
}
return dao.db.Create(user).Error
}
// GetUserByID 根据ID获取用户
func (dao *UserDAO) GetByID(id int64) (*model.User, error) {
var user model.User
err := dao.db.First(&user, id).Error
if err != nil {
return nil, err
}
return &user, nil
}
// GetUserByUsername 根据用户名获取用户
func (dao *UserDAO) GetByUsername(username string) (*model.User, error) {
var user model.User
err := dao.db.Where("user_name = ?", username).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
// UpdateUser 更新用户信息
func (dao *UserDAO) Update(user *model.User) error {
if user == nil {
return errors.New("user is nil")
}
user.UpdatedAt = time.Now()
return dao.db.Save(user).Error
}
// DeleteUser 删除用户
func (dao *UserDAO) Delete(id int64) error {
return dao.db.Delete(&model.User{}, id).Error
// return dao.db.Model(&model.User{}).Where("id = ?", id).Update("status", 2).Error
}
// ListUsers 获取用户列表
func (dao *UserDAO) List(offset, limit int) ([]model.User, error) {
var users []model.User
err := dao.db.Offset(offset).Limit(limit).Find(&users).Error
if err != nil {
return nil, err
}
return users, nil
}
// EnableUser 启用User
func (dao *UserDAO) Enable(id int64) error {
return dao.db.Model(&model.User{}).Where("id = ?", id).Update("status", 0).Error
}
// DisableUser 禁用User
func (dao *UserDAO) Disable(id int64) error {
return dao.db.Model(&model.User{}).Where("id = ?", id).Update("status", 1).Error
}
// BatchEnableUsers 批量启用User
func (dao *UserDAO) BatchEnable(ids []int64) error {
if len(ids) == 0 {
return errors.New("ids is empty")
}
return dao.db.Model(&model.User{}).Where("id IN ?", ids).Update("status", 0).Error
}
// BatchDisableUsers 批量禁用User
func (dao *UserDAO) BatchDisable(ids []int64) error {
if len(ids) == 0 {
return errors.New("ids is empty")
}
return dao.db.Model(&model.User{}).Where("id IN ?", ids).Update("status", 1).Error
}
// BatchDeleteUser 批量删除用户
func (dao *UserDAO) BatchDelete(ids []int64) error {
if len(ids) == 0 {
return errors.New("ids is empty")
}
return dao.db.Where("id IN ?", ids).Delete(&model.User{}).Error
// return dao.db.Model(&model.User{}).Where("id IN ?", ids).Update("status", 2).Error
}

182
team/key.go Normal file
View File

@@ -0,0 +1,182 @@
package team
import (
"net/http"
"opencatd-open/pkg/azureopenai"
"opencatd-open/store"
"strings"
"github.com/Sakurasan/to"
"github.com/gin-gonic/gin"
)
type Key struct {
ID int `json:"id,omitempty"`
Key string `json:"key,omitempty"`
Name string `json:"name,omitempty"`
ApiType string `json:"api_type,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
UpdatedAt string `json:"updatedAt,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
}
func HandleKeys(c *gin.Context) {
keys, err := store.GetAllKeys()
if err != nil {
c.JSON(http.StatusOK, gin.H{
"error": err.Error(),
})
}
c.JSON(http.StatusOK, keys)
}
func HandleAddKey(c *gin.Context) {
var body Key
if err := c.BindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{
"message": err.Error(),
}})
return
}
body.Name = strings.ToLower(strings.TrimSpace(body.Name))
body.Key = strings.TrimSpace(body.Key)
if strings.HasPrefix(body.Name, "azure.") {
keynames := strings.Split(body.Name, ".")
if len(keynames) < 2 {
c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{
"message": "Invalid Key Name",
}})
return
}
k := &store.Key{
ApiType: "azure",
Name: body.Name,
Key: body.Key,
ResourceNmae: keynames[1],
EndPoint: body.Endpoint,
}
if err := store.CreateKey(k); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{
"message": err.Error(),
}})
return
}
} else if strings.HasPrefix(body.Name, "claude.") {
keynames := strings.Split(body.Name, ".")
if len(keynames) < 2 {
c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{
"message": "Invalid Key Name",
}})
return
}
if body.Endpoint == "" {
body.Endpoint = "https://api.anthropic.com"
}
k := &store.Key{
// ApiType: "anthropic",
ApiType: "claude",
Name: body.Name,
Key: body.Key,
ResourceNmae: keynames[1],
EndPoint: body.Endpoint,
}
if err := store.CreateKey(k); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{
"message": err.Error(),
}})
return
}
} else if strings.HasPrefix(body.Name, "google.") {
keynames := strings.Split(body.Name, ".")
if len(keynames) < 2 {
c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{
"message": "Invalid Key Name",
}})
return
}
k := &store.Key{
// ApiType: "anthropic",
ApiType: "google",
Name: body.Name,
Key: body.Key,
ResourceNmae: keynames[1],
EndPoint: body.Endpoint,
}
if err := store.CreateKey(k); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{
"message": err.Error(),
}})
return
}
} else if strings.HasPrefix(body.Name, "github.") {
keynames := strings.Split(body.Name, ".")
if len(keynames) < 2 {
c.JSON(http.StatusBadRequest, gin.H{"error": gin.H{
"message": "Invalid Key Name",
}})
return
}
k := &store.Key{
ApiType: "github",
Name: body.Name,
Key: body.Key,
ResourceNmae: keynames[1],
EndPoint: body.Endpoint,
}
if err := store.CreateKey(k); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{
"message": err.Error(),
}})
return
}
} else {
if body.ApiType == "" {
if err := store.AddKey("openai", body.Key, body.Name); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{
"message": err.Error(),
}})
return
}
} else {
k := &store.Key{
ApiType: body.ApiType,
Name: body.Name,
Key: body.Key,
ResourceNmae: azureopenai.GetResourceName(body.Endpoint),
EndPoint: body.Endpoint,
}
if err := store.CreateKey(k); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{
"message": err.Error(),
}})
return
}
}
}
k, err := store.GetKeyrByName(body.Name)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": gin.H{
"message": err.Error(),
}})
return
}
c.JSON(http.StatusOK, k)
}
func HandleDelKey(c *gin.Context) {
id := to.Int(c.Param("id"))
if id < 1 {
c.JSON(http.StatusOK, gin.H{"error": "invalid key id"})
return
}
if err := store.DeleteKey(uint(id)); err != nil {
c.JSON(http.StatusOK, gin.H{"error": "invalid key id"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "ok"})
}

89
team/me.go Normal file
View File

@@ -0,0 +1,89 @@
package team
import (
"errors"
"net/http"
"opencatd-open/store"
"time"
"github.com/Sakurasan/to"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gorm.io/gorm"
)
func Handleinit(c *gin.Context) {
user, err := store.GetUserByID(1)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
u := store.User{Name: "root", Token: uuid.NewString()}
u.ID = 1
if err := store.CreateUser(&u); err != nil {
c.JSON(http.StatusForbidden, gin.H{
"error": err.Error(),
})
return
} else {
rootToken = u.Token
c.JSON(http.StatusOK, u)
return
}
}
c.JSON(http.StatusOK, gin.H{
"error": err.Error(),
})
return
}
if user.ID == 1 {
c.JSON(http.StatusForbidden, gin.H{
"error": "super user already exists, use cli to reset password",
})
}
}
func HandleMe(c *gin.Context) {
token := c.GetHeader("Authorization")
u, err := store.GetUserByToken(token[7:])
if err != nil {
c.JSON(http.StatusOK, gin.H{
"error": err.Error(),
})
}
c.JSON(http.StatusOK, u)
}
func HandleMeUsage(c *gin.Context) {
token := c.GetHeader("Authorization")
fromStr := c.Query("from")
toStr := c.Query("to")
getMonthStartAndEnd := func() (start, end string) {
loc, _ := time.LoadLocation("Local")
now := time.Now().In(loc)
year, month, _ := now.Date()
startOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, loc)
endOfMonth := startOfMonth.AddDate(0, 1, 0)
start = startOfMonth.Format("2006-01-02")
end = endOfMonth.Format("2006-01-02")
return
}
if fromStr == "" || toStr == "" {
fromStr, toStr = getMonthStartAndEnd()
}
user, err := store.GetUserByToken(token)
if err != nil {
c.AbortWithError(http.StatusForbidden, err)
return
}
usage, err := store.QueryUserUsage(to.String(user.ID), fromStr, toStr)
if err != nil {
c.AbortWithError(http.StatusForbidden, err)
return
}
c.JSON(200, usage)
}

69
team/middleware.go Normal file
View File

@@ -0,0 +1,69 @@
package team
import (
"log"
"net/http"
"opencatd-open/store"
"strings"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
var (
rootToken string
)
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if rootToken == "" {
u, err := store.GetUserByID(uint(1))
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
rootToken = u.Token
}
token := c.GetHeader("Authorization")
if token == "" || token[:7] != "Bearer " {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
if store.IsExistAuthCache(token[7:]) {
if strings.HasPrefix(c.Request.URL.Path, "/1/me") {
c.Next()
return
}
}
if token[7:] != rootToken {
u, err := store.GetUserByID(uint(1))
if err != nil {
log.Println(err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
if token[:7] != u.Token {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
rootToken = u.Token
store.LoadAuthCache()
}
// 可以在这里对 token 进行验证并检查权限
c.Next()
}
}
func CORS() gin.HandlerFunc {
config := cors.DefaultConfig()
config.AllowAllOrigins = true
config.AllowCredentials = true
config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}
config.AllowHeaders = []string{"*"}
return cors.New(config)
}

19
team/model/Token.go Normal file
View File

@@ -0,0 +1,19 @@
package model
// 用户的token
type Token struct {
Id int64 `gorm:"column:id;primaryKey;autoIncrement"`
UserId int64 `gorm:"column:user_id;not null;index:idx_token_user_id"`
Name string `gorm:"column:name;index:idx_token_name"`
Key string `gorm:"column:key;type:char(48);uniqueIndex:idx_token_key"`
Status bool `gorm:"column:status;default:true"` // enabled 0, disabled 1
Quota int64 `gorm:"column:quota;type:bigint;default:0"` // -1 means unlimited
UnlimitedQuota bool `gorm:"column:unlimited_quota;default:true"`
UsedQuota int64 `gorm:"column:used_quota;type:bigint;default:0"`
CreatedTime int64 `gorm:"column:created_time;type:bigint"`
ExpiredTime int64 `gorm:"column:expired_time;type:bigint;default:-1"` // -1 means never expired
}
func (Token) TableName() string {
return "token"
}

49
team/model/apikey.go Normal file
View File

@@ -0,0 +1,49 @@
package model
import (
"time"
"github.com/lib/pq"
)
type ApiKey_PG struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement"`
Name string `gorm:"column:name;not null;unique;index:idx_apikey_name"`
ApiType string `gorm:"column:apitype;not null;unique;index:idx_apikey_apitype"`
ApiKey string `gorm:"column:apikey;not null;unique;index:idx_apikey_apikey"`
Status int `gorm:"type:int;default:0"` // enabled 0, disabled 1
Endpoint string `gorm:"column:endpoint"`
ResourceNmae string `gorm:"column:resource_name"`
DeploymentName string `gorm:"column:deployment_name"`
ApiSecret string `gorm:"column:api_secret"`
ModelPrefix string `gorm:"column:model_prefix"`
ModelAlias string `gorm:"column:model_alias"`
SupportModels pq.StringArray `gorm:"type:text[]"`
UpdatedAt time.Time `json:"updatedAt,omitempty"`
CreatedAt time.Time `json:"createdAt,omitempty"`
}
func (ApiKey_PG) TableName() string {
return "apikeys"
}
type ApiKey struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement"`
Name string `gorm:"column:name;not null;unique;index:idx_apikey_name"`
ApiType string `gorm:"column:apitype;not null;unique;index:idx_apikey_apitype"`
ApiKey string `gorm:"column:apikey;not null;unique;index:idx_apikey_apikey"`
Status int `json:"status" gorm:"type:int;default:0"` // enabled 0, disabled 1
Endpoint string `gorm:"column:endpoint"`
ResourceNmae string `gorm:"column:resource_name"`
DeploymentName string `gorm:"column:deployment_name"`
ApiSecret string `gorm:"column:api_secret"`
ModelPrefix string `gorm:"column:model_prefix"`
ModelAlias string `gorm:"column:model_alias"`
SupportModels []string `gorm:"type:json"`
CreatedAt time.Time `json:"created_at,omitempty" gorm:"autoUpdateTime"`
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"autoCreateTime"`
}
func (ApiKey) TableName() string {
return "apikeys"
}

72
team/model/usage.go Normal file
View File

@@ -0,0 +1,72 @@
package model
import (
"net/http"
"opencatd-open/store"
"time"
"github.com/gin-gonic/gin"
)
type DailyUsage struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement"`
UserID int64 `gorm:"column:user_id;index:idx_user_id"`
TokenId int64 `gorm:"column:token_id;index:idx_token_id"`
Capability string `gorm:"column:capability;index:idx_capability;comment:模型能力"`
Date time.Time `gorm:"column:date;autoCreateTime;index:idx_date"`
Model string `gorm:"column:model"`
Stream bool `gorm:"column:stream"`
PromptTokens int `gorm:"column:prompt_tokens"`
CompletionTokens int `gorm:"column:completion_tokens"`
TotalTokens int `gorm:"column:total_tokens"`
Cost string `gorm:"column:cost"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
}
func (DailyUsage) TableName() string {
return "daily_usages"
}
type Usage struct {
ID int `gorm:"column:id"`
UserID int `gorm:"column:user_id"`
SKU string `gorm:"column:sku"`
PromptUnits int `gorm:"column:prompt_units"`
CompletionUnits int `gorm:"column:completion_units"`
TotalUnit int `gorm:"column:total_unit"`
Cost string `gorm:"column:cost"`
Date time.Time `gorm:"column:date"`
}
func (Usage) TableName() string {
return "usages"
}
func HandleUsage(c *gin.Context) {
fromStr := c.Query("from")
toStr := c.Query("to")
getMonthStartAndEnd := func() (start, end string) {
loc, _ := time.LoadLocation("Local")
now := time.Now().In(loc)
year, month, _ := now.Date()
startOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, loc)
endOfMonth := startOfMonth.AddDate(0, 1, 0)
start = startOfMonth.Format("2006-01-02")
end = endOfMonth.Format("2006-01-02")
return
}
if fromStr == "" || toStr == "" {
fromStr, toStr = getMonthStartAndEnd()
}
usage, err := store.QueryUsage(fromStr, toStr)
if err != nil {
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return
}
c.JSON(200, usage)
}

113
team/model/user.go Normal file
View File

@@ -0,0 +1,113 @@
package model
import (
"net/http"
"opencatd-open/store"
"time"
"github.com/Sakurasan/to"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type User struct {
ID int64 `json:"id" gorm:"primaryKey;autoIncrement"`
Name string `json:"name" gorm:"unique;index" validate:"max=12"`
UserName string `json:"username" gorm:"unique;index" validate:"max=12"`
Password string `json:"password" gorm:"not null;" validate:"min=8,max=20"`
Role int `json:"role" gorm:"type:int;default:1"` // default user
Status int `json:"status" gorm:"type:int;default:0"` // enabled 0, disabled 1 deleted 2
Nickname string `json:"nickname" gorm:"type:varchar(50)"`
AvatarURL string `json:"avatar_url" gorm:"type:varchar(255)"`
Email string `json:"email" gorm:"type:varchar(255);unique;index"`
Quota int64 `json:"quota" gorm:"bigint;default:-1"` // default unlimited
Token string `json:"token,omitempty"`
Timezone string `json:"timezone" gorm:"type:varchar(50)"`
Language string `json:"language" gorm:"type:varchar(50)"`
CreatedAt time.Time `json:"created_at,omitempty" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"autoUpdateTime"`
}
func HandleUsers(c *gin.Context) {
users, err := store.GetAllUsers()
if err != nil {
c.JSON(http.StatusOK, gin.H{
"error": err.Error(),
})
}
c.JSON(http.StatusOK, users)
}
func HandleAddUser(c *gin.Context) {
var body User
if err := c.BindJSON(&body); err != nil {
c.JSON(http.StatusOK, gin.H{"error": err.Error()})
return
}
if len(body.Name) == 0 {
c.JSON(http.StatusOK, gin.H{"error": "invalid user name"})
return
}
if err := store.AddUser(body.Name, uuid.NewString()); err != nil {
c.JSON(http.StatusOK, gin.H{"error": err.Error()})
return
}
u, err := store.GetUserByName(body.Name)
if err != nil {
c.JSON(http.StatusOK, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, u)
}
func HandleDelUser(c *gin.Context) {
id := to.Int(c.Param("id"))
if id <= 1 {
c.JSON(http.StatusOK, gin.H{"error": "invalid user id"})
return
}
if err := store.DeleteUser(uint(id)); err != nil {
c.JSON(http.StatusOK, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "ok"})
}
func HandleResetUserToken(c *gin.Context) {
id := to.Int(c.Param("id"))
newtoken := c.Query("token")
if newtoken == "" {
newtoken = uuid.NewString()
}
if err := store.UpdateUser(uint(id), newtoken); err != nil {
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return
}
u, err := store.GetUserByID(uint(id))
if err != nil {
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, u)
}
type Session struct {
ID int64 `json:"id" gorm:"primaryKey;autoIncrement"`
UserID int64 `json:"user_id" gorm:"index:idx_user_id"`
Token string `json:"token" gorm:"type:varchar(64);uniqueIndex"`
DeviceType string `json:"device_type" gorm:"type:varchar(100);default:''"`
DeviceName string `json:"device_name" gorm:"type:varchar(100);default:''"`
LastActiveAt time.Time `json:"last_active_at" gorm:"type:timestamp;default:CURRENT_TIMESTAMP"`
LogoutAt time.Time `json:"logout_at" gorm:"type:timestamp;null"`
CreatedAt time.Time `json:"created_at" gorm:"type:timestamp;not null;default:CURRENT_TIMESTAMP"`
UpdatedAt time.Time `json:"updated_at" gorm:"type:timestamp;not null;default:CURRENT_TIMESTAMP;update:CURRENT_TIMESTAMP"`
}
func (Session) TableName() string {
return "sessions"
}