package gift import ( "context" "encoding/json" "errors" "gorm.io/gorm" "gorm.io/gorm/clause" bizgift "kra/internal/biz/gift" "kra/internal/data/ledger" "kra/internal/utils/serial" "strconv" "strings" "time" ) type Provider interface{ DB() *gorm.DB } type giftPO struct { ID uint `gorm:"primaryKey"` CreatedAt time.Time UpdatedAt time.Time `gorm:"index"` DeletedAt gorm.DeletedAt `gorm:"index"` Name string `gorm:"size:60;uniqueIndex"` Icon string `gorm:"size:1000"` Price int64 `gorm:"index"` Description string `gorm:"size:255"` Tiers string `gorm:"type:text"` Sort int `gorm:"default:0;index"` Enabled bool `gorm:"index"` } func (giftPO) TableName() string { return "live_gifts" } type repo struct{ data Provider } func NewRepo(data Provider) bizgift.Repo { return &repo{data: data} } func (r *repo) db(ctx context.Context) *gorm.DB { return r.data.DB().WithContext(ctx) } func (r *repo) List(ctx context.Context, f bizgift.Filter, o bizgift.ListOptions) ([]*bizgift.Gift, int64, error) { q := r.db(ctx).Model(&giftPO{}) if f.Name != "" { q = q.Where("name LIKE ?", "%"+f.Name+"%") } if f.MinPrice > 0 { q = q.Where("price >= ?", f.MinPrice) } if f.MaxPrice > 0 { q = q.Where("price <= ?", f.MaxPrice) } if f.Enabled != nil { q = q.Where("enabled = ?", *f.Enabled) } var total int64 if err := q.Count(&total).Error; err != nil { return nil, 0, err } var rows []giftPO if err := q.Order(giftOrder(o.OrderBy)).Offset(o.Offset).Limit(o.Limit).Find(&rows).Error; err != nil { return nil, 0, err } items := make([]*bizgift.Gift, 0, len(rows)) for i := range rows { item := toBiz(&rows[i]) r.db(ctx).Model(&ledger.GiftSendPO{}).Where("gift_id = ?", item.ID).Count(&item.SendCount) items = append(items, item) } return items, total, nil } func (r *repo) Get(ctx context.Context, id uint) (*bizgift.Gift, error) { var row giftPO if err := r.db(ctx).First(&row, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, bizgift.ErrNotFound } return nil, err } item := toBiz(&row) r.db(ctx).Model(&ledger.GiftSendPO{}).Where("gift_id = ?", id).Count(&item.SendCount) return item, nil } func (r *repo) Save(ctx context.Context, item *bizgift.Gift) (*bizgift.Gift, error) { row := newPO(item) if item.ID == 0 { if err := r.db(ctx).Create(row).Error; err != nil { return nil, mapError(err) } item.ID = row.ID } else { if !item.Enabled { if err := r.notLast(ctx, item.ID); err != nil { return nil, err } } res := r.db(ctx).Model(&giftPO{}).Where("id = ?", item.ID).Updates(map[string]any{"name": item.Name, "icon": item.Icon, "price": item.Price, "description": item.Description, "tiers": encode(item.Tiers), "sort": item.Sort, "enabled": item.Enabled}) if res.Error != nil { return nil, mapError(res.Error) } if res.RowsAffected == 0 { return nil, bizgift.ErrNotFound } } return r.Get(ctx, item.ID) } func (r *repo) Delete(ctx context.Context, id uint) error { if err := r.notLast(ctx, id); err != nil { return err } res := r.db(ctx).Delete(&giftPO{}, id) if res.Error != nil { return res.Error } if res.RowsAffected == 0 { return bizgift.ErrNotFound } return nil } func (r *repo) SetEnabled(ctx context.Context, id uint, enabled bool) error { if !enabled { if err := r.notLast(ctx, id); err != nil { return err } } res := r.db(ctx).Model(&giftPO{}).Where("id = ?", id).Update("enabled", enabled) if res.Error != nil { return res.Error } if res.RowsAffected == 0 { return bizgift.ErrNotFound } return nil } func (r *repo) SaveSort(ctx context.Context, values map[uint]int) error { return r.db(ctx).Transaction(func(tx *gorm.DB) error { for id, v := range values { if err := tx.Model(&giftPO{}).Where("id = ?", id).Update("sort", v).Error; err != nil { return err } } return nil }) } func (r *repo) notLast(ctx context.Context, id uint) error { var row giftPO if err := r.db(ctx).First(&row, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return bizgift.ErrNotFound } return err } if !row.Enabled { return nil } var count int64 if err := r.db(ctx).Model(&giftPO{}).Where("enabled = ?", true).Count(&count).Error; err != nil { return err } if count <= 1 { return bizgift.ErrLastEnabled } return nil } type sendRow struct { ledger.GiftSendPO Username string `gorm:"column:username"` Nickname string `gorm:"column:nickname"` RoomDeleted bool `gorm:"column:room_deleted"` } func (r *repo) ListSends(ctx context.Context, f bizgift.SendFilter, o bizgift.ListOptions) ([]*bizgift.Send, int64, *bizgift.Summary, error) { q := sendQuery(r.db(ctx), f) var total int64 if err := q.Count(&total).Error; err != nil { return nil, 0, nil, err } var sum struct { Diamonds int64 AmountCents int64 } if err := sendQuery(r.db(ctx), f).Select("COALESCE(SUM(live_gift_sends.diamond_total),0) AS diamonds,COALESCE(SUM(live_gift_sends.equivalent_cents),0) AS amount_cents").Scan(&sum).Error; err != nil { return nil, 0, nil, err } var rows []sendRow if err := q.Select("live_gift_sends.*,sys_users.username,sys_users.nick_name AS nickname,CASE WHEN live_rooms.id IS NULL THEN 1 ELSE 0 END AS room_deleted").Order(sendOrder(o.OrderBy)).Offset(o.Offset).Limit(o.Limit).Scan(&rows).Error; err != nil { return nil, 0, nil, err } items := make([]*bizgift.Send, 0, len(rows)) for i := range rows { items = append(items, sendToBiz(&rows[i])) } return items, total, &bizgift.Summary{Count: total, Diamonds: sum.Diamonds, AmountCents: sum.AmountCents}, nil } func sendQuery(db *gorm.DB, f bizgift.SendFilter) *gorm.DB { q := db.Model(&ledger.GiftSendPO{}).Joins("LEFT JOIN sys_users ON sys_users.id = live_gift_sends.user_id").Joins("LEFT JOIN live_rooms ON live_rooms.id = live_gift_sends.room_id AND live_rooms.deleted_at IS NULL") if f.RoomName != "" { q = q.Where("live_gift_sends.room_name LIKE ?", "%"+f.RoomName+"%") } if f.AnchorName != "" { q = q.Where("live_gift_sends.anchor_name LIKE ?", "%"+f.AnchorName+"%") } if f.UserKeyword != "" { v := "%" + f.UserKeyword + "%" q = q.Where("sys_users.username LIKE ? OR sys_users.nick_name LIKE ?", v, v) } if f.GiftID > 0 { q = q.Where("live_gift_sends.gift_id = ?", f.GiftID) } if len(f.CreatedAt) == 2 { q = q.Where("live_gift_sends.created_at BETWEEN ? AND ?", f.CreatedAt[0], f.CreatedAt[1]) } return q } func (r *repo) Send(ctx context.Context, userID, roomID, giftID uint, quantity int) (*bizgift.Send, error) { var created uint err := r.db(ctx).Transaction(func(tx *gorm.DB) error { var room struct { ID uint Name string AnchorUserID uint AnchorName string } if err := tx.Table("live_rooms").Select("live_rooms.id,live_rooms.name,live_rooms.anchor_user_id,COALESCE(NULLIF(sys_users.nick_name,''),sys_users.username) AS anchor_name").Joins("LEFT JOIN sys_users ON sys_users.id = live_rooms.anchor_user_id").Where("live_rooms.id = ? AND live_rooms.enabled = ? AND live_rooms.deleted_at IS NULL", roomID, true).Scan(&room).Error; err != nil { return err } if room.ID == 0 { return bizgift.ErrSendNotAllowed } var gift giftPO if err := tx.Where("id = ? AND enabled = ?", giftID, true).First(&gift).Error; err != nil { return bizgift.ErrSendNotAllowed } wallet := ledger.WalletPO{UserID: userID} if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("user_id = ?", userID).FirstOrCreate(&wallet).Error; err != nil { return err } total := gift.Price * int64(quantity) if wallet.DiamondBalance < total { return &bizgift.InsufficientBalanceError{Required: total, Balance: wallet.DiamondBalance, Missing: total - wallet.DiamondBalance} } rate := int64(10) var setting ledger.SettingPO if err := tx.Where(&ledger.SettingPO{Key: "diamond_rate_cents"}).First(&setting).Error; err == nil { if v, e := strconv.ParseInt(setting.Value, 10, 64); e == nil { rate = v } } roomIDCopy, giftIDCopy := room.ID, gift.ID serialNo, err := serial.New("DS") if err != nil { return err } row := ledger.GiftSendPO{SerialNo: serialNo, UserID: userID, RoomID: &roomIDCopy, RoomName: room.Name, AnchorUserID: room.AnchorUserID, AnchorName: room.AnchorName, GiftID: &giftIDCopy, GiftName: gift.Name, GiftIcon: gift.Icon, GiftPrice: gift.Price, Quantity: quantity, DiamondTotal: total, RateCents: rate, EquivalentCents: total * rate} if err := tx.Create(&row).Error; err != nil { return err } if err := tx.Model(&wallet).Updates(map[string]any{"diamond_balance": gorm.Expr("diamond_balance - ?", total), "total_spent_diamonds": gorm.Expr("total_spent_diamonds + ?", total)}).Error; err != nil { return err } created = row.ID return nil }) if err != nil { return nil, err } var row sendRow if err := r.db(ctx).Model(&ledger.GiftSendPO{}).Select("live_gift_sends.*,sys_users.username,sys_users.nick_name AS nickname,0 AS room_deleted").Joins("LEFT JOIN sys_users ON sys_users.id = live_gift_sends.user_id").Where("live_gift_sends.id = ?", created).Scan(&row).Error; err != nil { return nil, err } return sendToBiz(&row), nil } func newPO(v *bizgift.Gift) *giftPO { return &giftPO{ID: v.ID, Name: v.Name, Icon: v.Icon, Price: v.Price, Description: v.Description, Tiers: encode(v.Tiers), Sort: v.Sort, Enabled: v.Enabled} } func toBiz(v *giftPO) *bizgift.Gift { return &bizgift.Gift{ID: v.ID, Name: v.Name, Icon: v.Icon, Price: v.Price, Description: v.Description, Tiers: decode(v.Tiers), Sort: v.Sort, Enabled: v.Enabled, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt} } func sendToBiz(v *sendRow) *bizgift.Send { return &bizgift.Send{ID: v.ID, SerialNo: v.SerialNo, UserID: v.UserID, Username: v.Username, Nickname: v.Nickname, RoomID: v.RoomID, RoomName: v.RoomName, RoomDeleted: v.RoomDeleted, AnchorUserID: v.AnchorUserID, AnchorName: v.AnchorName, GiftID: v.GiftID, GiftName: v.GiftName, GiftIcon: v.GiftIcon, GiftPrice: v.GiftPrice, Quantity: v.Quantity, DiamondTotal: v.DiamondTotal, RateCents: v.RateCents, EquivalentCents: v.EquivalentCents, CreatedAt: v.CreatedAt} } func encode(v []int) string { b, _ := json.Marshal(v); return string(b) } func decode(v string) []int { var out []int; _ = json.Unmarshal([]byte(v), &out); return out } func mapError(err error) error { s := strings.ToLower(err.Error()) if strings.Contains(s, "unique") || strings.Contains(s, "duplicate") { return bizgift.ErrNameExists } return err } func giftOrder(v string) string { m := map[string]string{"sort_asc": "sort ASC", "sort_desc": "sort DESC", "name_asc": "name ASC", "name_desc": "name DESC", "price_asc": "price ASC", "price_desc": "price DESC", "updated_at_asc": "updated_at ASC", "updated_at_desc": "updated_at DESC", "created_at_asc": "created_at ASC", "created_at_desc": "created_at DESC"} if x := m[strings.ToLower(v)]; x != "" { return x } return "sort ASC,created_at DESC,id ASC" } func sendOrder(v string) string { m := map[string]string{"created_at_asc": "live_gift_sends.created_at ASC", "created_at_desc": "live_gift_sends.created_at DESC", "price_asc": "live_gift_sends.gift_price ASC", "price_desc": "live_gift_sends.gift_price DESC", "quantity_asc": "live_gift_sends.quantity ASC", "quantity_desc": "live_gift_sends.quantity DESC", "total_asc": "live_gift_sends.diamond_total ASC", "total_desc": "live_gift_sends.diamond_total DESC"} if x := m[strings.ToLower(v)]; x != "" { return x } return "live_gift_sends.created_at DESC,live_gift_sends.id DESC" }