thread-master/apps/backend/internal/module/usage/repository/mongo.go

214 lines
6.2 KiB
Go

package repository
import (
"context"
"fmt"
libmongo "apps/backend/internal/lib/mongo"
"apps/backend/internal/module/usage/domain"
"github.com/zeromicro/go-zero/core/stores/mon"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type MonStore struct {
events *mon.Model
counters *mon.Model
prefs *mon.Model
purchases *mon.Model
}
func NewMonStore(uri, database string) *MonStore {
uri = libmongo.MustMongoURI(uri)
return &MonStore{
events: mon.MustNewModel(uri, database, "usage_events"),
counters: mon.MustNewModel(uri, database, "usage_monthly_counters"),
prefs: mon.MustNewModel(uri, database, "usage_prefs"),
purchases: mon.MustNewModel(uri, database, "usage_purchases"),
}
}
func monthlyCounterID(uid int64, monthKey string) string {
return fmt.Sprintf("%d:%s", uid, monthKey)
}
func meterCounterField(meter string) (string, error) {
switch meter {
case domain.MeterAICopy, domain.MeterAIResearch, domain.MeterWebSearch, domain.MeterAIImage:
return "by_meter." + meter, nil
default:
return "", fmt.Errorf("invalid usage meter %q", meter)
}
}
func (s *MonStore) ensureMonthlyCounter(ctx context.Context, uid int64, monthKey string) error {
id := monthlyCounterID(uid, monthKey)
var existing domain.MonthlyCounter
if err := s.counters.FindOne(ctx, &existing, bson.M{"_id": id}); err == nil {
return nil
} else if err != mon.ErrNotFound {
return err
}
events, err := s.ListEvents(ctx, uid, monthKey, domain.KeyModePlatform, 0)
if err != nil {
return err
}
counter := &domain.MonthlyCounter{
ID: id, UID: uid, MonthKey: monthKey, ByMeter: map[string]int{}, UpdatedAt: domain.NowNano(),
}
for _, event := range events {
credits := event.Credits
if minimum := domain.MeterCost(event.Meter); credits < minimum {
credits = minimum
}
counter.TotalCredits += credits
counter.ByMeter[event.Meter] += credits
}
if _, err := s.counters.InsertOne(ctx, counter); err != nil && !mongo.IsDuplicateKeyError(err) {
return err
}
return nil
}
func (s *MonStore) ReservePlatform(ctx context.Context, uid int64, monthKey, meter string, cost, totalLimit, meterLimit int) error {
field, err := meterCounterField(meter)
if err != nil {
return err
}
if totalLimit >= 0 && cost > totalLimit {
return domain.ErrQuotaExceeded
}
if meterLimit >= 0 && cost > meterLimit {
return domain.ErrMeterCapExceeded
}
if err := s.ensureMonthlyCounter(ctx, uid, monthKey); err != nil {
return err
}
filter := bson.M{"_id": monthlyCounterID(uid, monthKey)}
if totalLimit >= 0 {
filter["total_credits"] = bson.M{"$lte": totalLimit - cost}
}
if meterLimit >= 0 {
filter["$or"] = bson.A{
bson.M{field: bson.M{"$exists": false}},
bson.M{field: bson.M{"$lte": meterLimit - cost}},
}
}
res, err := s.counters.UpdateOne(ctx, filter, bson.M{
"$inc": bson.M{"total_credits": cost, field: cost},
"$set": bson.M{"updated_at": domain.NowNano()},
})
if err != nil {
return err
}
if res.MatchedCount != 0 {
return nil
}
counter, err := s.GetMonthlyCounter(ctx, uid, monthKey)
if err != nil {
return err
}
if totalLimit >= 0 && counter.TotalCredits+cost > totalLimit {
return domain.ErrQuotaExceeded
}
if meterLimit >= 0 && counter.ByMeter[meter]+cost > meterLimit {
return domain.ErrMeterCapExceeded
}
return fmt.Errorf("platform quota reservation did not match counter")
}
func (s *MonStore) GetMonthlyCounter(ctx context.Context, uid int64, monthKey string) (*domain.MonthlyCounter, error) {
var counter domain.MonthlyCounter
err := s.counters.FindOne(ctx, &counter, bson.M{"_id": monthlyCounterID(uid, monthKey)})
if err == mon.ErrNotFound {
return nil, domain.ErrNotFound
}
if err != nil {
return nil, err
}
if counter.ByMeter == nil {
counter.ByMeter = map[string]int{}
}
return &counter, nil
}
func (s *MonStore) InsertEvent(ctx context.Context, e *domain.Event) error {
_, err := s.events.InsertOne(ctx, e)
return err
}
func (s *MonStore) ListEvents(ctx context.Context, uid int64, monthKey, keyMode string, limit int) ([]*domain.Event, error) {
filter := bson.M{"uid": uid}
if monthKey != "" {
filter["month_key"] = monthKey
}
if keyMode != "" && keyMode != "all" {
filter["key_mode"] = keyMode
}
opts := options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}})
if limit > 0 {
opts.SetLimit(int64(limit))
}
var list []*domain.Event
err := s.events.Find(ctx, &list, filter, opts)
return list, err
}
func (s *MonStore) ListEventsAll(ctx context.Context, monthKey string, limit int) ([]*domain.Event, error) {
filter := bson.M{}
if monthKey != "" {
filter["month_key"] = monthKey
}
opts := options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}})
if limit > 0 {
opts.SetLimit(int64(limit))
}
var list []*domain.Event
err := s.events.Find(ctx, &list, filter, opts)
return list, err
}
func (s *MonStore) ListEventsRange(ctx context.Context, fromNano, toExclusiveNano int64) ([]*domain.Event, error) {
var list []*domain.Event
err := s.events.Find(ctx, &list, bson.M{
"created_at": bson.M{"$gte": fromNano, "$lt": toExclusiveNano},
}, options.Find().SetSort(bson.D{{Key: "created_at", Value: 1}}))
return list, err
}
func (s *MonStore) GetPrefs(ctx context.Context, uid int64) (*domain.MemberPrefs, error) {
var p domain.MemberPrefs
err := s.prefs.FindOne(ctx, &p, bson.M{"uid": uid})
if err != nil {
if err == mon.ErrNotFound {
return &domain.MemberPrefs{UID: uid, PlanID: domain.PlanFree}, nil
}
return nil, err
}
return &p, nil
}
func (s *MonStore) SavePrefs(ctx context.Context, p *domain.MemberPrefs) error {
_, err := s.prefs.ReplaceOne(ctx, bson.M{"uid": p.UID}, p, options.Replace().SetUpsert(true))
return err
}
func (s *MonStore) InsertPurchase(ctx context.Context, p *domain.Purchase) error {
_, err := s.purchases.InsertOne(ctx, p)
return err
}
func (s *MonStore) ListPurchases(ctx context.Context, uid int64, limit int) ([]*domain.Purchase, error) {
opts := options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}})
if limit > 0 {
opts.SetLimit(int64(limit))
}
var list []*domain.Purchase
err := s.purchases.Find(ctx, &list, bson.M{"uid": uid}, opts)
return list, err
}
var _ domain.Repository = (*MonStore)(nil)