package gift import ( "context" "fmt" "sort" "strings" "time" kratoserrors "github.com/go-kratos/kratos/v3/errors" bizlive "kra/internal/biz/live" ) var ( ErrNotFound = kratoserrors.NotFound("GIFT_NOT_FOUND", "gift not found") ErrNameExists = kratoserrors.BadRequest("GIFT_NAME_EXISTS", "gift name already exists") ErrInvalid = kratoserrors.BadRequest("GIFT_INVALID", "invalid gift data") ErrLastEnabled = kratoserrors.BadRequest("GIFT_LAST_ENABLED", "at least one enabled gift is required") ErrInsufficientBalance = kratoserrors.BadRequest("GIFT_INSUFFICIENT_BALANCE", "insufficient diamond balance") ErrSendNotAllowed = kratoserrors.BadRequest("GIFT_SEND_NOT_ALLOWED", "gift cannot be sent") ) // InsufficientBalanceError preserves the wallet snapshot needed by clients // to decide whether to open the recharge flow. It unwraps to the stable // ErrInsufficientBalance reason so existing errors.Is checks keep working. type InsufficientBalanceError struct { Required int64 Balance int64 Missing int64 } func (e *InsufficientBalanceError) Error() string { if e == nil { return ErrInsufficientBalance.Error() } return fmt.Sprintf("insufficient diamond balance: need %d, have %d, missing %d", e.Required, e.Balance, e.Missing) } func (e *InsufficientBalanceError) Unwrap() error { return ErrInsufficientBalance } type Gift struct { ID uint Name, Icon, Description string Price int64 Tiers []int Sort int Enabled bool SendCount int64 CreatedAt, UpdatedAt time.Time } type Send struct { ID uint SerialNo string UserID uint Username, Nickname string RoomID *uint RoomName string RoomDeleted bool AnchorUserID uint AnchorName string GiftID *uint GiftName, GiftIcon string GiftPrice int64 Quantity int DiamondTotal, RateCents, EquivalentCents int64 CreatedAt time.Time } type Filter struct { Name string MinPrice, MaxPrice int64 Enabled *bool } type SendFilter struct { RoomName, AnchorName, UserKeyword string GiftID uint CreatedAt []time.Time } type ListOptions struct { Offset, Limit int OrderBy string } type Summary struct{ Count, Diamonds, AmountCents int64 } type Repo interface { List(context.Context, Filter, ListOptions) ([]*Gift, int64, error) Get(context.Context, uint) (*Gift, error) Save(context.Context, *Gift) (*Gift, error) Delete(context.Context, uint) error SetEnabled(context.Context, uint, bool) error SaveSort(context.Context, map[uint]int) error ListSends(context.Context, SendFilter, ListOptions) ([]*Send, int64, *Summary, error) Send(context.Context, uint, uint, uint, int) (*Send, error) } type Notifier interface { GiftSent(context.Context, *Send) } type roomAvailability interface { IsLive(context.Context, uint) (bool, error) } type Usecase struct { repo Repo notifier Notifier rooms roomAvailability } func NewUsecase(repo Repo, notifier Notifier, rooms *bizlive.RoomUsecase) *Usecase { return newUsecase(repo, notifier, rooms) } func newUsecase(repo Repo, notifier Notifier, rooms roomAvailability) *Usecase { return &Usecase{repo: repo, notifier: notifier, rooms: rooms} } func (uc *Usecase) List(ctx context.Context, f Filter, o ListOptions) ([]*Gift, int64, error) { f.Name = strings.TrimSpace(f.Name) return uc.repo.List(ctx, f, normalize(o)) } func (uc *Usecase) Get(ctx context.Context, id uint) (*Gift, error) { if id == 0 { return nil, ErrNotFound } return uc.repo.Get(ctx, id) } func (uc *Usecase) Save(ctx context.Context, item *Gift) (*Gift, error) { if err := validate(item); err != nil { return nil, err } return uc.repo.Save(ctx, item) } func (uc *Usecase) Delete(ctx context.Context, id uint) error { return uc.repo.Delete(ctx, id) } func (uc *Usecase) SetEnabled(ctx context.Context, id uint, enabled bool) error { return uc.repo.SetEnabled(ctx, id, enabled) } func (uc *Usecase) SaveSort(ctx context.Context, values map[uint]int) error { for id, v := range values { if id == 0 || v < 0 || v > 9999 { return ErrInvalid } } return uc.repo.SaveSort(ctx, values) } func (uc *Usecase) ListSends(ctx context.Context, f SendFilter, o ListOptions) ([]*Send, int64, *Summary, error) { return uc.repo.ListSends(ctx, f, normalize(o)) } func (uc *Usecase) Send(ctx context.Context, userID, roomID, giftID uint, quantity int) (*Send, error) { if userID == 0 || roomID == 0 || giftID == 0 || quantity < 1 || quantity > 9999 { return nil, ErrSendNotAllowed } if uc.rooms == nil { return nil, ErrSendNotAllowed } live, err := uc.rooms.IsLive(ctx, roomID) if err != nil || !live { return nil, ErrSendNotAllowed } result, err := uc.repo.Send(ctx, userID, roomID, giftID, quantity) if err != nil { return nil, err } if uc.notifier != nil { uc.notifier.GiftSent(ctx, result) } return result, nil } func normalize(o ListOptions) ListOptions { if o.Offset < 0 { o.Offset = 0 } if o.Limit < 1 || o.Limit > 100 { o.Limit = 10 } return o } func validate(item *Gift) error { if item == nil { return ErrInvalid } item.Name = strings.TrimSpace(item.Name) item.Icon = strings.TrimSpace(item.Icon) item.Description = strings.TrimSpace(item.Description) if len([]rune(item.Name)) < 1 || len([]rune(item.Name)) > 12 || item.Icon == "" || item.Price < 1 || item.Price > 99999 || len([]rune(item.Description)) > 50 || item.Sort < 0 || item.Sort > 9999 || len(item.Tiers) < 1 || len(item.Tiers) > 6 { return ErrInvalid } seen := map[int]struct{}{} for _, v := range item.Tiers { if v < 1 || v > 9999 { return ErrInvalid } if _, ok := seen[v]; ok { return ErrInvalid } seen[v] = struct{}{} } sort.Ints(item.Tiers) return nil }