feat: live module and jenkins pipeline to harbor deploy

This commit is contained in:
Yvan
2026-09-21 16:45:44 +08:00
parent 451c8e2abc
commit 829d75c657
145 changed files with 8984 additions and 269 deletions
+165
View File
@@ -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")
}
+72
View File
@@ -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)
}
}
+5
View File
@@ -0,0 +1,5 @@
package liveclient
import "github.com/google/wire"
var ProviderSet = wire.NewSet(NewRepo)