package liveclient import ( "context" "errors" "strconv" "strings" "time" "github.com/google/uuid" "gorm.io/gorm" biz "kra/internal/biz/liveclient" "kra/internal/data/ledger" ) type Provider interface{ DB() *gorm.DB } type repo struct{ data Provider } func NewRepo(data Provider) biz.Repo { return &repo{data: data} } type userPO struct { ID uint `gorm:"primaryKey"` CreatedAt time.Time UpdatedAt time.Time DeletedAt gorm.DeletedAt `gorm:"index"` UUID string Username string Password string NickName string `gorm:"column:nick_name"` HeaderImg string `gorm:"column:header_img"` AuthorityID uint `gorm:"column:authority_id"` Enable int PasswordUpdatedAt *time.Time MustChangePassword bool } func (userPO) TableName() string { return "sys_users" } type authorityPO struct { AuthorityID uint `gorm:"primaryKey;column:authority_id"` AuthorityName string ParentID *uint DataScope int DefaultRouter string } func (authorityPO) TableName() string { return "sys_authorities" } type userAuthorityPO struct { UserID uint `gorm:"column:sys_user_id"` AuthorityID uint `gorm:"column:sys_authority_authority_id"` } func (userAuthorityPO) TableName() string { return "sys_user_authority" } type policyPO struct { ID uint `gorm:"primaryKey;autoIncrement"` Ptype string V0, V1, V2 string V3, V4, V5 string } func (policyPO) TableName() string { return "casbin_rule" } func (r *repo) Register(ctx context.Context, account *biz.Account, passwordHash string, initialDiamonds int64) (*biz.Account, error) { now := time.Now() user := userPO{UUID: uuid.NewString(), Username: account.Username, Password: passwordHash, NickName: account.Nickname, HeaderImg: account.Avatar, AuthorityID: biz.ClientAuthorityID, Enable: 1, PasswordUpdatedAt: &now} err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { var count int64 if err := tx.Model(&userPO{}).Where("username = ?", account.Username).Count(&count).Error; err != nil { return err } if count > 0 { return biz.ErrUsernameExists } root := uint(0) authority := authorityPO{AuthorityID: biz.ClientAuthorityID, AuthorityName: "直播用户", ParentID: &root, DataScope: 4, DefaultRouter: "404"} if err := tx.Where("authority_id = ?", authority.AuthorityID).FirstOrCreate(&authority).Error; err != nil { return err } for _, route := range clientRoutes() { policy := policyPO{Ptype: "p", V0: strconv.FormatUint(uint64(biz.ClientAuthorityID), 10), V1: route.path, V2: route.method} if err := tx.Where("ptype = ? AND v0 = ? AND v1 = ? AND v2 = ?", policy.Ptype, policy.V0, policy.V1, policy.V2).FirstOrCreate(&policy).Error; err != nil { return err } } if err := tx.Create(&user).Error; err != nil { if duplicate(err) { return biz.ErrUsernameExists } return err } if err := tx.Create(&userAuthorityPO{UserID: user.ID, AuthorityID: biz.ClientAuthorityID}).Error; err != nil { return err } return tx.Create(&ledger.WalletPO{UserID: user.ID, DiamondBalance: initialDiamonds}).Error }) if err != nil { return nil, err } return r.Get(ctx, user.ID) } func clientRoutes() []struct{ method, path string } { return []struct{ method, path string }{ {method: "POST", path: "/live/client/auth/logout"}, {method: "GET", path: "/live/client/me"}, {method: "GET", path: "/live/client/balance"}, {method: "PUT", path: "/live/client/me"}, {method: "PUT", path: "/live/client/me/password"}, {method: "POST", path: "/live/rooms/:id/gifts"}, {method: "GET", path: "/live/rooms/:id/follow"}, {method: "POST", path: "/live/rooms/:id/follow"}, {method: "DELETE", path: "/live/rooms/:id/follow"}, {method: "POST", path: "/live/recharge/orders"}, {method: "GET", path: "/live/recharge/orders/:orderNo"}, {method: "GET", path: "/live/recharge/orders/:orderNo/status"}, } } func (r *repo) Get(ctx context.Context, userID uint) (*biz.Account, error) { var row struct { ID uint CreatedAt time.Time UUID string Username string Nickname string Avatar string AuthorityID uint Enable int DiamondBalance int64 PasswordUpdatedAt *time.Time } err := r.data.DB().WithContext(ctx).Table("sys_users"). Select("sys_users.id, sys_users.created_at, sys_users.uuid, sys_users.username, sys_users.nick_name AS nickname, sys_users.header_img AS avatar, sys_users.authority_id, sys_users.enable, COALESCE(live_wallets.diamond_balance, 0) AS diamond_balance, sys_users.password_updated_at"). Joins("LEFT JOIN live_wallets ON live_wallets.user_id = sys_users.id"). Where("sys_users.id = ? AND sys_users.deleted_at IS NULL", userID).Scan(&row).Error if err != nil { return nil, err } if row.ID == 0 { return nil, biz.ErrAccountNotFound } return &biz.Account{UserID: row.ID, UUID: row.UUID, Username: row.Username, Nickname: row.Nickname, Avatar: row.Avatar, AuthorityID: row.AuthorityID, Enabled: row.Enable == 1, DiamondBalance: row.DiamondBalance, CreatedAt: row.CreatedAt, PasswordUpdatedAt: row.PasswordUpdatedAt}, nil } func (r *repo) UpdateProfile(ctx context.Context, in *biz.ProfileInput) (*biz.Account, error) { result := r.data.DB().WithContext(ctx).Model(&userPO{}).Where("id = ?", in.UserID).Updates(map[string]any{"nick_name": in.Nickname, "header_img": in.Avatar}) if result.Error != nil { return nil, result.Error } if result.RowsAffected == 0 { var count int64 if err := r.data.DB().WithContext(ctx).Model(&userPO{}).Where("id = ?", in.UserID).Count(&count).Error; err != nil { return nil, err } if count == 0 { return nil, biz.ErrAccountNotFound } } return r.Get(ctx, in.UserID) } func duplicate(err error) bool { if err == nil || errors.Is(err, biz.ErrUsernameExists) { return false } text := strings.ToLower(err.Error()) return strings.Contains(text, "unique") || strings.Contains(text, "duplicate") }