Files
kra-new/internal/data/gift/gift.go
T
2026-09-23 17:42:55 +08:00

301 lines
11 KiB
Go

package gift
import (
"context"
"encoding/json"
"errors"
"fmt"
"gorm.io/gorm"
"gorm.io/gorm/clause"
bizgift "kra/internal/biz/gift"
"kra/internal/data/ledger"
"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:"default:true;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
row := ledger.GiftSendPO{SerialNo: newNumber("DS"), 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"
}
func newNumber(prefix string) string {
return fmt.Sprintf("%s%s%06d", prefix, time.Now().Format("20060102150405"), time.Now().UnixNano()%1000000)
}