Files
team-api/internal/logic/open/open_api.go
T

555 lines
14 KiB
Go

package open
import (
"context"
"fmt"
"strings"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
v1 "github.com/qianfree/team-api/api/open/v1"
"github.com/qianfree/team-api/internal/dao"
"github.com/qianfree/team-api/internal/logic/relay"
"github.com/qianfree/team-api/internal/middleware"
do "github.com/qianfree/team-api/internal/model/do"
"github.com/qianfree/team-api/internal/service"
"github.com/qianfree/team-api/internal/utility/crypto"
)
type sOpen struct{}
func New() *sOpen {
return &sOpen{}
}
func init() {
service.RegisterOpen(New())
}
// ============================================================
// 成员管理
// ============================================================
func (s *sOpen) OpenMemberList(ctx context.Context, req *v1.OpenMemberListReq) (*v1.OpenMemberListRes, error) {
tenantID := middleware.GetOpenTenantID(ctx)
if tenantID == 0 {
return nil, fmt.Errorf("unauthorized")
}
page := req.Page
pageSize := req.PageSize
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
m := dao.TntUsers.Ctx(ctx).Where("tenant_id", tenantID)
if req.Keyword != "" {
kw := "%" + strings.TrimSpace(req.Keyword) + "%"
m = m.Where("username LIKE ? OR email LIKE ?", kw, kw)
}
if req.Role != "" {
m = m.Where("role", req.Role)
}
if req.Status != "" {
m = m.Where("status", req.Status)
}
total, err := m.Count()
if err != nil {
return nil, err
}
var users []struct {
Id int64 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Role string `json:"role"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
}
err = m.OrderDesc("id").Page(page, pageSize).Scan(&users)
if err != nil {
return nil, err
}
items := make([]v1.OpenMemberItem, len(users))
for i, u := range users {
items[i] = v1.OpenMemberItem{
ID: u.Id,
Username: u.Username,
Email: u.Email,
Role: u.Role,
Status: u.Status,
CreatedAt: u.CreatedAt,
}
}
return &v1.OpenMemberListRes{List: items, Total: total, Page: page, PageSize: pageSize}, nil
}
func (s *sOpen) OpenMemberCreate(ctx context.Context, req *v1.OpenMemberCreateReq) (*v1.OpenMemberCreateRes, error) {
tenantID := middleware.GetOpenTenantID(ctx)
if tenantID == 0 {
return nil, fmt.Errorf("unauthorized")
}
// Check duplicate email
count, _ := dao.TntUsers.Ctx(ctx).Where("tenant_id", tenantID).Where("email", req.Email).Count()
if count > 0 {
return nil, fmt.Errorf("邮箱已存在")
}
passwordHash, err := crypto.HashPassword(req.Password)
if err != nil {
return nil, err
}
result, err := dao.TntUsers.Ctx(ctx).Data(do.TntUsers{
TenantId: tenantID,
Username: req.Username,
Email: req.Email,
PasswordHash: passwordHash,
Role: req.Role,
DisplayName: req.DisplayName,
Status: "active",
}).Insert()
if err != nil {
return nil, err
}
id, _ := result.LastInsertId()
return &v1.OpenMemberCreateRes{ID: id}, nil
}
func (s *sOpen) OpenMemberUpdate(ctx context.Context, req *v1.OpenMemberUpdateReq) (*v1.OpenMemberUpdateRes, error) {
tenantID := middleware.GetOpenTenantID(ctx)
if tenantID == 0 {
return nil, fmt.Errorf("unauthorized")
}
data := g.Map{}
if req.Role != nil {
data["role"] = *req.Role
}
if req.DisplayName != nil {
data["display_name"] = *req.DisplayName
}
if req.Status != nil {
data["status"] = *req.Status
}
if len(data) == 0 {
return nil, nil
}
_, err := dao.TntUsers.Ctx(ctx).Where("id", req.Id).Where("tenant_id", tenantID).Data(data).Update()
return nil, err
}
func (s *sOpen) OpenMemberDelete(ctx context.Context, req *v1.OpenMemberDeleteReq) (*v1.OpenMemberDeleteRes, error) {
tenantID := middleware.GetOpenTenantID(ctx)
if tenantID == 0 {
return nil, fmt.Errorf("unauthorized")
}
// Prevent deleting owner
var user *struct {
Role string `json:"role"`
}
err := dao.TntUsers.Ctx(ctx).Where("id", req.Id).Where("tenant_id", tenantID).Scan(&user)
if err != nil {
return nil, err
}
if user == nil {
return nil, fmt.Errorf("用户不存在")
}
if user.Role == "owner" {
return nil, fmt.Errorf("不能删除所有者")
}
_, err = dao.TntUsers.Ctx(ctx).Where("id", req.Id).Where("tenant_id", tenantID).Delete()
return nil, err
}
// ============================================================
// 成员额度 & 模型
// ============================================================
func (s *sOpen) OpenMemberQuota(ctx context.Context, req *v1.OpenMemberQuotaReq) (*v1.OpenMemberQuotaRes, error) {
tenantID := middleware.GetOpenTenantID(ctx)
if tenantID == 0 {
return nil, fmt.Errorf("unauthorized")
}
var user struct {
QuotaType string `json:"quota_type"`
QuotaLimit float64 `json:"quota_limit"`
QuotaUsed float64 `json:"quota_used"`
QuotaPeriod string `json:"quota_period"`
}
err := dao.TntUsers.Ctx(ctx).
Where("id", req.Id).
Where("tenant_id", tenantID).
Fields("quota_type, quota_limit, quota_used, quota_period").
Scan(&user)
if err != nil {
return nil, err
}
return &v1.OpenMemberQuotaRes{
QuotaType: user.QuotaType,
QuotaLimit: user.QuotaLimit,
QuotaUsed: user.QuotaUsed,
Period: user.QuotaPeriod,
}, nil
}
func (s *sOpen) OpenMemberQuotaUpdate(ctx context.Context, req *v1.OpenMemberQuotaUpdateReq) (*v1.OpenMemberQuotaUpdateRes, error) {
tenantID := middleware.GetOpenTenantID(ctx)
if tenantID == 0 {
return nil, fmt.Errorf("unauthorized")
}
if req.QuotaType == "periodic" && req.Period == "" {
return nil, fmt.Errorf("period is required for periodic quota")
}
data := g.Map{
"quota_type": req.QuotaType,
"quota_limit": req.QuotaLimit,
"quota_used": 0,
"quota_period": nil,
"quota_reset_at": nil,
}
if req.QuotaType == "periodic" {
data["quota_period"] = req.Period
data["quota_reset_at"] = gtime.Now()
}
if req.QuotaType == "none" {
data["quota_limit"] = 0
}
_, err := dao.TntUsers.Ctx(ctx).
Where("id", req.Id).
Where("tenant_id", tenantID).
Data(data).
Update()
return nil, err
}
func (s *sOpen) OpenMemberModels(ctx context.Context, req *v1.OpenMemberModelsReq) (*v1.OpenMemberModelsRes, error) {
tenantID := middleware.GetOpenTenantID(ctx)
if tenantID == 0 {
return nil, fmt.Errorf("unauthorized")
}
var scopes []struct {
ModelID string `json:"model_id"`
}
err := dao.TntMemberModelScopes.Ctx(ctx).
Where("tenant_id", tenantID).
Where("user_id", req.Id).
Scan(&scopes)
if err != nil {
return nil, err
}
modelIDs := make([]string, 0, len(scopes))
for _, s := range scopes {
modelIDs = append(modelIDs, s.ModelID)
}
return &v1.OpenMemberModelsRes{List: modelIDs}, nil
}
func (s *sOpen) OpenMemberModelsUpdate(ctx context.Context, req *v1.OpenMemberModelsUpdateReq) (*v1.OpenMemberModelsUpdateRes, error) {
tenantID := middleware.GetOpenTenantID(ctx)
if tenantID == 0 {
return nil, fmt.Errorf("unauthorized")
}
// Delete existing scopes
_, err := dao.TntMemberModelScopes.Ctx(ctx).
Where("tenant_id", tenantID).
Where("user_id", req.Id).
Delete()
if err != nil {
return nil, err
}
// Insert new scopes
for _, modelID := range req.ModelIDs {
if modelID == 0 {
continue
}
_, err = dao.TntMemberModelScopes.Ctx(ctx).Data(do.TntMemberModelScopes{
TenantId: tenantID,
UserId: req.Id,
ModelId: modelID,
}).Insert()
if err != nil {
return nil, err
}
}
return nil, nil
}
// ============================================================
// API Key 管理
// ============================================================
func (s *sOpen) OpenKeyList(ctx context.Context, req *v1.OpenKeyListReq) (*v1.OpenKeyListRes, error) {
tenantID := middleware.GetOpenTenantID(ctx)
if tenantID == 0 {
return nil, fmt.Errorf("unauthorized")
}
page := req.Page
pageSize := req.PageSize
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
m := dao.ApiKeys.Ctx(ctx).Where("tenant_id", tenantID)
if req.Status != "" {
m = m.Where("status", req.Status)
}
total, err := m.Count()
if err != nil {
return nil, err
}
var keys []struct {
Id int64 `json:"id"`
Name string `json:"name"`
KeyPrefix string `json:"key_prefix"`
Status string `json:"status"`
CreatedAt *gtime.Time `json:"created_at"`
}
err = m.OrderDesc("id").Page(page, pageSize).Scan(&keys)
if err != nil {
return nil, err
}
items := make([]v1.OpenKeyItem, len(keys))
for i, k := range keys {
items[i] = v1.OpenKeyItem{
ID: k.Id,
Name: k.Name,
Key: k.KeyPrefix + "***",
Status: k.Status,
}
if k.CreatedAt != nil {
items[i].CreatedAt = k.CreatedAt.Format("Y-m-d H:i:s")
}
}
return &v1.OpenKeyListRes{List: items, Total: total, Page: page, PageSize: pageSize}, nil
}
func (s *sOpen) OpenKeyCreate(ctx context.Context, req *v1.OpenKeyCreateReq) (*v1.OpenKeyCreateRes, error) {
tenantID := middleware.GetOpenTenantID(ctx)
if tenantID == 0 {
return nil, fmt.Errorf("unauthorized")
}
// Generate API key
rawKey, prefix, encryptedKey, err := relay.GenerateApiKey(ctx)
if err != nil {
return nil, err
}
quotaLimit := req.QuotaLimit
if quotaLimit == "" {
quotaLimit = "0"
}
result, err := dao.ApiKeys.Ctx(ctx).Data(do.ApiKeys{
TenantId: tenantID,
Name: req.Name,
EncryptedKey: encryptedKey,
KeyPrefix: prefix,
Scope: "full",
Status: "active",
TotalQuota: quotaLimit,
KeyType: "project",
}).Insert()
if err != nil {
return nil, err
}
id, _ := result.LastInsertId()
// Set model scopes if provided
for _, modelName := range req.ModelScopes {
if modelName != "" {
dao.ApiKeyModelScopes.Ctx(ctx).Insert(do.ApiKeyModelScopes{
ApiKeyId: id,
ModelName: modelName,
})
}
}
return &v1.OpenKeyCreateRes{ID: id, Key: rawKey}, nil
}
func (s *sOpen) OpenKeyDelete(ctx context.Context, req *v1.OpenKeyDeleteReq) (*v1.OpenKeyDeleteRes, error) {
tenantID := middleware.GetOpenTenantID(ctx)
if tenantID == 0 {
return nil, fmt.Errorf("unauthorized")
}
_, err := dao.ApiKeys.Ctx(ctx).Where("id", req.Id).Where("tenant_id", tenantID).Delete()
return nil, err
}
// ============================================================
// 用量查询
// ============================================================
func (s *sOpen) OpenUsageQuery(ctx context.Context, req *v1.OpenUsageQueryReq) (*v1.OpenUsageQueryRes, error) {
tenantID := middleware.GetOpenTenantID(ctx)
if tenantID == 0 {
return nil, fmt.Errorf("unauthorized")
}
page := req.Page
pageSize := req.PageSize
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
m := dao.BilUsageLogs.Ctx(ctx).Where("tenant_id", tenantID)
m = m.Where("created_at >= ?", req.StartDate+" 00:00:00")
m = m.Where("created_at <= ?", req.EndDate+" 23:59:59")
// Group by
selectFields := ""
groupBy := ""
switch req.GroupBy {
case "model":
selectFields = "model_name, SUM(request_count) as request_count, SUM(prompt_tokens) as prompt_tokens, SUM(completion_tokens) as completion_tokens, SUM(total_tokens) as total_tokens, SUM(cost) as cost"
groupBy = "model_name"
case "key":
selectFields = "key_prefix as key_name, SUM(request_count) as request_count, SUM(prompt_tokens) as prompt_tokens, SUM(completion_tokens) as completion_tokens, SUM(total_tokens) as total_tokens, SUM(cost) as cost"
groupBy = "key_prefix"
default:
selectFields = "DATE(created_at) as date, SUM(request_count) as request_count, SUM(prompt_tokens) as prompt_tokens, SUM(completion_tokens) as completion_tokens, SUM(total_tokens) as total_tokens, SUM(cost) as cost"
groupBy = "DATE(created_at)"
}
type usageRow struct {
Date string `json:"date"`
Model string `json:"model_name"`
KeyName string `json:"key_name"`
RequestCount int64 `json:"request_count"`
PromptTokens int64 `json:"prompt_tokens"`
CompletionTokens int64 `json:"completion_tokens"`
TotalTokens int64 `json:"total_tokens"`
Cost float64 `json:"cost"`
}
var rows []usageRow
err := m.Fields(selectFields).Group(groupBy).OrderDesc(groupBy).Page(page, pageSize).Scan(&rows)
if err != nil {
return nil, err
}
items := make([]v1.OpenUsageItem, 0, len(rows))
for _, r := range rows {
item := v1.OpenUsageItem{
RequestCount: r.RequestCount,
PromptTokens: r.PromptTokens,
CompletionTokens: r.CompletionTokens,
TotalTokens: r.TotalTokens,
Cost: fmt.Sprintf("%.6f", r.Cost),
}
switch req.GroupBy {
case "model":
item.Model = r.Model
case "key":
item.KeyName = r.KeyName
default:
item.Date = r.Date
}
items = append(items, item)
}
return &v1.OpenUsageQueryRes{List: items, Total: len(items), Page: page, PageSize: pageSize}, nil
}
// ============================================================
// 费用查询
// ============================================================
func (s *sOpen) OpenBillingQuery(ctx context.Context, req *v1.OpenBillingQueryReq) (*v1.OpenBillingQueryRes, error) {
tenantID := middleware.GetOpenTenantID(ctx)
if tenantID == 0 {
return nil, fmt.Errorf("unauthorized")
}
page := req.Page
pageSize := req.PageSize
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
m := dao.BilRecords.Ctx(ctx).Where("tenant_id", tenantID)
m = m.Where("created_at >= ?", req.StartDate+" 00:00:00")
m = m.Where("created_at <= ?", req.EndDate+" 23:59:59")
total, err := m.Count()
if err != nil {
return nil, err
}
type billingRow struct {
Id int64 `json:"id"`
Type string `json:"type"`
TotalCost float64 `json:"total_cost"`
Balance float64 `json:"balance"`
ModelName string `json:"model_name"`
CreatedAt *gtime.Time `json:"created_at"`
}
var records []billingRow
err = m.Fields("id, relay_mode as type, total_cost, 0 as balance, model_name, created_at").
OrderDesc("created_at").
Page(page, pageSize).
Scan(&records)
if err != nil {
return nil, err
}
items := make([]v1.OpenBillingItem, 0, len(records))
for _, r := range records {
item := v1.OpenBillingItem{
ID: r.Id,
Type: r.Type,
Amount: fmt.Sprintf("%.6f", r.TotalCost),
Description: r.ModelName,
}
if r.CreatedAt != nil {
item.CreatedAt = r.CreatedAt.Format("Y-m-d H:i:s")
}
items = append(items, item)
}
return &v1.OpenBillingQueryRes{List: items, Total: total, Page: page, PageSize: pageSize}, nil
}