package dao import ( "context" "opencatd-open/internal/store" "time" "gorm.io/gorm" "gorm.io/gorm/clause" ) type UsageDAO struct { db *gorm.DB } type DailyUsageDAO struct { db *gorm.DB } func NewUsageDAO(db *gorm.DB) *UsageDAO { return &UsageDAO{db: db} } func NewDailyUsageDAO(db *gorm.DB) *DailyUsageDAO { return &DailyUsageDAO{db: db} } // UsageLog DAO func (d *UsageDAO) Create(ctx context.Context, log *store.UsageLog) error { return d.db.WithContext(ctx).Create(log).Error } func (d *UsageDAO) BatchCreate(ctx context.Context, logs []*store.UsageLog) error { return d.db.WithContext(ctx).Create(logs).Error } func (d *UsageDAO) ListByUserID(ctx context.Context, userID uint64, limit, offset int) ([]*store.UsageLog, error) { var logs []*store.UsageLog err := d.db.WithContext(ctx). Where("user_id = ?", userID). Order("created_at DESC"). Limit(limit). Offset(offset). Find(&logs).Error return logs, err } func (d *UsageDAO) Delete(ctx context.Context, id uint64) error { return d.db.WithContext(ctx).Delete(&store.UsageLog{}, id).Error } func (d *UsageDAO) CountByUserID(ctx context.Context, userID uint64) (int64, error) { var count int64 err := d.db.WithContext(ctx).Model(&store.UsageLog{}).Where("user_id = ?", userID).Count(&count).Error return count, err } // UsageDaily DAO func (d *DailyUsageDAO) Create(ctx context.Context, log *store.UsageDaily) error { return d.db.WithContext(ctx).Create(log).Error } func (d *DailyUsageDAO) ListByUserID(ctx context.Context, userID uint64, limit, offset int) ([]*store.UsageDaily, error) { var logs []*store.UsageDaily err := d.db.WithContext(ctx). Where("user_id = ?", userID). Order("date DESC"). Limit(limit). Offset(offset). Find(&logs).Error return logs, err } func (d *DailyUsageDAO) GetByDate(ctx context.Context, userID uint64, date string) (*store.UsageDaily, error) { var log store.UsageDaily err := d.db.WithContext(ctx). Where("user_id = ? AND date = ?", userID, date). First(&log).Error if err != nil { return nil, err } return &log, nil } // UpsertDailyUsage 按 (user_id, model_id, date) 累加式 upsert: // 行不存在则插入;存在则在原值基础上增量累加(不能用 AssignmentColumns 覆盖, // 否则多次 flush 会互相清零)。非限定列名在 SQLite/MySQL/PG 的 upsert 语义下都指向目标行。 func (d *DailyUsageDAO) UpsertDailyUsage(ctx context.Context, log *store.UsageDaily) error { return d.db.WithContext(ctx).Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "user_id"}, {Name: "model_id"}, {Name: "date"}}, DoUpdates: clause.Assignments(map[string]interface{}{ "requests": gorm.Expr("requests + ?", log.Requests), "input_tokens": gorm.Expr("input_tokens + ?", log.InputTokens), "output_tokens": gorm.Expr("output_tokens + ?", log.OutputTokens), "cache_read_tokens": gorm.Expr("cache_read_tokens + ?", log.CacheReadTokens), "cost": gorm.Expr("cost + ?", log.Cost), }), }).Create(log).Error } func (d *DailyUsageDAO) ListByDateRange(ctx context.Context, userID uint64, start, end time.Time) ([]*store.UsageDaily, error) { var logs []*store.UsageDaily err := d.db.WithContext(ctx). Where("user_id = ? AND date >= ? AND date <= ?", userID, start.Format("2006-01-02"), end.Format("2006-01-02")). Order("date DESC"). Find(&logs).Error return logs, err }