196 lines
6.1 KiB
Go
196 lines
6.1 KiB
Go
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
|
|
}
|