Files
team-api/internal/logic/open/open_api.go
T
2026-06-03 15:34:38 +08:00

602 lines
16 KiB
Go

package open
import (
"context"
"fmt"
"strings"
"time"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"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/consts"
"github.com/qianfree/team-api/internal/dao"
"github.com/qianfree/team-api/internal/logic/common"
"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{}
var openMemberModelCache = common.NewCache("member_model", 60*time.Second)
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, errOpen(consts.CodeUnauthorized, "未认证")
}
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, errOpen(consts.CodeUnauthorized, "未认证")
}
count, _ := dao.TntUsers.Ctx(ctx).Where("tenant_id", tenantID).Where("email", req.Email).Count()
if count > 0 {
return nil, errOpen(consts.CodeUsernameExists, "邮箱已存在")
}
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, errOpen(consts.CodeUnauthorized, "未认证")
}
data := do.TntUsers{}
hasUpdate := false
if req.Role != nil {
data.Role = *req.Role
hasUpdate = true
}
if req.DisplayName != nil {
data.DisplayName = *req.DisplayName
hasUpdate = true
}
if req.Status != nil {
data.Status = *req.Status
hasUpdate = true
}
if !hasUpdate {
return nil, nil
}
_, err := dao.TntUsers.Ctx(ctx).Where("id", req.Id).Where("tenant_id", tenantID).OmitNil().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, errOpen(consts.CodeUnauthorized, "未认证")
}
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, errOpen(consts.CodeNotFound, "用户不存在")
}
if user.Role == "owner" {
return nil, errOpen(consts.CodeForbidden, "不能删除所有者")
}
_, 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, errOpen(consts.CodeUnauthorized, "未认证")
}
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
}
if user == nil {
return nil, errOpen(consts.CodeNotFound, "用户不存在")
}
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, errOpen(consts.CodeUnauthorized, "未认证")
}
if req.QuotaType == "periodic" && req.Period == "" {
return nil, errOpen(consts.CodeBadRequest, "periodic 类型额度必须指定周期")
}
data := do.TntUsers{
QuotaType: req.QuotaType,
QuotaUsed: 0,
}
if req.QuotaType != "none" {
data.QuotaLimit = req.QuotaLimit
} else {
data.QuotaLimit = 0
}
if req.QuotaType == "periodic" {
data.QuotaPeriod = req.Period
data.QuotaResetAt = gtime.Now()
}
_, 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, errOpen(consts.CodeUnauthorized, "未认证")
}
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, errOpen(consts.CodeUnauthorized, "未认证")
}
err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
_, err := tx.Model("tnt_member_model_scopes").Ctx(ctx).
Where("tenant_id", tenantID).
Where("user_id", req.Id).
Delete()
if err != nil {
return err
}
if len(req.ModelIDs) > 0 {
for _, modelID := range req.ModelIDs {
if modelID == 0 {
continue
}
_, err = tx.Model("tnt_member_model_scopes").Ctx(ctx).Data(do.TntMemberModelScopes{
TenantId: tenantID,
UserId: req.Id,
ModelId: modelID,
}).Insert()
if err != nil {
return err
}
}
} else {
_, err = tx.Model("tnt_member_model_scopes").Ctx(ctx).Data(do.TntMemberModelScopes{
TenantId: tenantID,
UserId: req.Id,
ModelId: -1,
}).Insert()
if err != nil {
return err
}
}
return nil
})
if err != nil {
return nil, err
}
cacheKey := fmt.Sprintf("%d:%d", tenantID, req.Id)
openMemberModelCache.Delete(ctx, cacheKey)
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, errOpen(consts.CodeUnauthorized, "未认证")
}
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, errOpen(consts.CodeUnauthorized, "未认证")
}
rawKey, prefix, encryptedKey, err := relay.GenerateApiKey(ctx)
if err != nil {
return nil, err
}
quotaLimit := req.QuotaLimit
if quotaLimit < 0 {
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()
for _, modelName := range req.ModelScopes {
if modelName != "" {
if _, err = dao.ApiKeyModelScopes.Ctx(ctx).Insert(do.ApiKeyModelScopes{
ApiKeyId: id,
ModelName: modelName,
}); err != nil {
return nil, gerror.Wrapf(err, "设置模型范围失败")
}
}
}
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, errOpen(consts.CodeUnauthorized, "未认证")
}
_, 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) {
if err := common.ValidateDateParam(req.StartDate, "开始日期"); err != nil {
return nil, err
}
if err := common.ValidateDateParam(req.EndDate, "结束日期"); err != nil {
return nil, err
}
tenantID := middleware.GetOpenTenantID(ctx)
if tenantID == 0 {
return nil, errOpen(consts.CodeUnauthorized, "未认证")
}
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")
selectFields := ""
groupBy := ""
switch req.GroupBy {
case "model":
selectFields = "model_name, COUNT(*) as request_count, SUM(input_tokens) as prompt_tokens, SUM(output_tokens) as completion_tokens, SUM(input_tokens+output_tokens) as total_tokens, SUM(actual_cost) as cost"
groupBy = "model_name"
case "key":
selectFields = "key_prefix as key_name, COUNT(*) as request_count, SUM(input_tokens) as prompt_tokens, SUM(output_tokens) as completion_tokens, SUM(input_tokens+output_tokens) as total_tokens, SUM(actual_cost) as cost"
groupBy = "key_prefix"
default:
selectFields = "DATE(created_at) as date, COUNT(*) as request_count, SUM(input_tokens) as prompt_tokens, SUM(output_tokens) as completion_tokens, SUM(input_tokens+output_tokens) as total_tokens, SUM(actual_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) {
if err := common.ValidateDateParam(req.StartDate, "开始日期"); err != nil {
return nil, err
}
if err := common.ValidateDateParam(req.EndDate, "结束日期"); err != nil {
return nil, err
}
tenantID := middleware.GetOpenTenantID(ctx)
if tenantID == 0 {
return nil, errOpen(consts.CodeUnauthorized, "未认证")
}
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
}
// errOpen is a helper to create open platform errors with proper gerror codes.
func errOpen(code int, message string) error {
return gerror.NewCode(gcode.New(code, message, nil), message)
}