feat: live module and jenkins pipeline to harbor deploy
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
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: "GET", path: "/live/client/me"},
|
||||
{method: "PUT", path: "/live/client/me"},
|
||||
{method: "PUT", path: "/live/client/me/password"},
|
||||
{method: "POST", path: "/live/rooms/:id/gifts"},
|
||||
{method: "POST", path: "/live/recharge/orders"},
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package liveclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
biz "kra/internal/biz/liveclient"
|
||||
"kra/internal/data/ledger"
|
||||
)
|
||||
|
||||
type testProvider struct{ db *gorm.DB }
|
||||
|
||||
func (p testProvider) DB() *gorm.DB { return p.db }
|
||||
|
||||
func openAccountTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = db.AutoMigrate(&userPO{}, &authorityPO{}, &userAuthorityPO{}, &policyPO{}, &ledger.WalletPO{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func TestRegisterCreatesCanonicalUserRoleWalletAndPolicies(t *testing.T) {
|
||||
db := openAccountTestDB(t)
|
||||
repo := NewRepo(testProvider{db: db})
|
||||
account, err := repo.Register(context.Background(), &biz.Account{Username: "viewer", Nickname: "Viewer"}, "password-hash", biz.InitialDiamonds)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if account.Username != "viewer" || account.AuthorityID != biz.ClientAuthorityID || account.DiamondBalance != biz.InitialDiamonds {
|
||||
t.Fatalf("account = %+v", account)
|
||||
}
|
||||
var user userPO
|
||||
var wallet ledger.WalletPO
|
||||
var links, policies int64
|
||||
if err = db.First(&user, account.UserID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = db.Where("user_id = ?", account.UserID).First(&wallet).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db.Model(&userAuthorityPO{}).Where("sys_user_id = ? AND sys_authority_authority_id = ?", account.UserID, biz.ClientAuthorityID).Count(&links)
|
||||
db.Model(&policyPO{}).Where("ptype = ? AND v0 = ?", "p", "1000").Count(&policies)
|
||||
if user.AuthorityID != biz.ClientAuthorityID || wallet.DiamondBalance != biz.InitialDiamonds || links != 1 || policies != int64(len(clientRoutes())) {
|
||||
t.Fatalf("registration state user=%+v wallet=%+v links=%d policies=%d", user, wallet, links, policies)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterDuplicateUsernameDoesNotCreateAnotherWallet(t *testing.T) {
|
||||
db := openAccountTestDB(t)
|
||||
repo := NewRepo(testProvider{db: db})
|
||||
if _, err := repo.Register(context.Background(), &biz.Account{Username: "viewer", Nickname: "Viewer"}, "hash", 100); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := repo.Register(context.Background(), &biz.Account{Username: "viewer", Nickname: "Other"}, "hash", 100); !errors.Is(err, biz.ErrUsernameExists) {
|
||||
t.Fatalf("duplicate error = %v", err)
|
||||
}
|
||||
var users, wallets int64
|
||||
db.Model(&userPO{}).Count(&users)
|
||||
db.Model(&ledger.WalletPO{}).Count(&wallets)
|
||||
if users != 1 || wallets != 1 {
|
||||
t.Fatalf("duplicate registration users=%d wallets=%d", users, wallets)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package liveclient
|
||||
|
||||
import "github.com/google/wire"
|
||||
|
||||
var ProviderSet = wire.NewSet(NewRepo)
|
||||
Reference in New Issue
Block a user