Files
opencatd-open/team/service/usage.go
Sakurasan bc223d6530 team api
2025-02-01 23:52:55 +08:00

138 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"fmt"
"opencatd-open/team/dao"
dto "opencatd-open/team/dto/team"
"opencatd-open/team/model"
"time"
"log"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
var _ UsageService = (*usageService)(nil)
type UsageService interface {
// AsyncProcessUsage 异步处理使用记录
AsyncProcessUsage(usage *model.Usage)
ListByUserID(ctx context.Context, userID int64, limit, offset int) ([]*model.Usage, error)
ListByCapability(ctx context.Context, capability string, limit, offset int) ([]*model.Usage, error)
ListByDateRange(ctx context.Context, start, end time.Time, filters map[string]interface{}) ([]*dto.UsageInfo, error)
Delete(ctx context.Context, id int64) error
}
type usageService struct {
db *gorm.DB
usageDAO dao.UsageRepository
dailyUsageDAO dao.DailyUsageRepository
usageChan chan *model.Usage // 用于异步处理的channel
ctx context.Context
}
func NewUsageService(ctx context.Context, db *gorm.DB, usageRepo dao.UsageRepository, dailyUsageRepo dao.DailyUsageRepository) UsageService {
srv := &usageService{
db: db,
usageDAO: usageRepo,
dailyUsageDAO: dailyUsageRepo,
usageChan: make(chan *model.Usage, 1000), // 设置合适的缓冲区大小
ctx: ctx,
}
// 启动异步处理goroutine
go srv.processUsageWorker()
return srv
}
func (s *usageService) AsyncProcessUsage(usage *model.Usage) {
select {
case s.usageChan <- usage:
// 成功发送到channel
default:
// channel已满记录错误日志
log.Println("usage channel is full, skip processing")
}
}
func (s *usageService) processUsageWorker() {
for {
select {
case usage := <-s.usageChan:
err := s.processUsage(usage)
if err != nil {
log.Println("processUsage error:", err)
}
case <-s.ctx.Done():
log.Println("processUsageWorker is exiting")
return
}
}
}
// processUsageWorker 异步处理worker
func (s *usageService) processUsage(usage *model.Usage) error {
err := s.db.Transaction(func(tx *gorm.DB) error {
// 1. 记录使用记录
if err := tx.WithContext(s.ctx).Create(usage).Error; err != nil {
return fmt.Errorf("create usage error: %w", err)
}
// 2. 更新每日统计upsert 操作)
dailyUsage := model.DailyUsage{
UserID: usage.UserID,
TokenID: usage.TokenID,
Capability: usage.Capability,
Date: time.Date(usage.Date.Year(), usage.Date.Month(), usage.Date.Day(), 0, 0, 0, 0, usage.Date.Location()),
Model: usage.Model,
Stream: usage.Stream,
PromptTokens: usage.PromptTokens,
CompletionTokens: usage.CompletionTokens,
TotalTokens: usage.TotalTokens,
Cost: usage.Cost,
}
// 使用 OnConflict 实现 upsert
if err := tx.WithContext(s.ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "user_id"}, {Name: "token_id"}, {Name: "capability"}, {Name: "date"}}, // 唯一键
DoUpdates: clause.Assignments(map[string]interface{}{
"prompt_tokens": gorm.Expr("prompt_tokens + ?", usage.PromptTokens),
"completion_tokens": gorm.Expr("completion_tokens + ?", usage.CompletionTokens),
"total_tokens": gorm.Expr("total_tokens + ?", usage.TotalTokens),
"cost": gorm.Expr("cost + ?", usage.Cost),
}),
}).Create(&dailyUsage).Error; err != nil {
return fmt.Errorf("upsert daily usage error: %w", err)
}
// 3. 更新用户额度
if err := tx.WithContext(s.ctx).Model(&model.User{}).Where("id = ?", usage.UserID).Update("quota", gorm.Expr("quota - ?", usage.Cost)).Error; err != nil {
return fmt.Errorf("update user quota error: %w", err)
}
return nil
})
return err
}
func (s *usageService) ListByUserID(ctx context.Context, userID int64, limit int, offset int) ([]*model.Usage, error) {
return s.usageDAO.ListByUserID(ctx, userID, limit, offset)
}
func (s *usageService) ListByCapability(ctx context.Context, capability string, limit, offset int) ([]*model.Usage, error) {
return s.usageDAO.ListByCapability(ctx, capability, limit, offset)
}
func (s *usageService) ListByDateRange(ctx context.Context, start, end time.Time, filters map[string]interface{}) ([]*dto.UsageInfo, error) {
return s.dailyUsageDAO.StatUserUsages(ctx, start, end, filters)
}
func (s *usageService) Delete(ctx context.Context, id int64) error {
return s.usageDAO.Delete(ctx, id)
}