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
+138
View File
@@ -0,0 +1,138 @@
package livechat
import (
"context"
"errors"
"time"
"gorm.io/gorm"
biz "kra/internal/biz/livechat"
)
type Provider interface{ DB() *gorm.DB }
type repo struct{ data Provider }
func NewRepo(data Provider) biz.Repo { return &repo{data: data} }
type messagePO struct {
ID uint `gorm:"primaryKey"`
RoomID uint `gorm:"index"`
Type string `gorm:"size:20;index"`
UserID uint `gorm:"index"`
Username string `gorm:"size:120"`
Nickname string `gorm:"size:120"`
Avatar string `gorm:"size:1000"`
Content string `gorm:"size:255"`
GiftID uint
GiftName string `gorm:"size:60"`
GiftIcon string `gorm:"size:1000"`
Quantity int
DiamondTotal int64
CreatedAt time.Time `gorm:"index"`
}
func (messagePO) TableName() string { return "live_chat_messages" }
type userSnapshot struct {
ID uint
Username string
Nickname string
Avatar string
}
func (r *repo) ListRecent(ctx context.Context, roomID uint, limit int) ([]*biz.Message, error) {
if err := r.ensureRoom(ctx, roomID); err != nil {
return nil, err
}
var rows []messagePO
if err := r.data.DB().WithContext(ctx).Where("room_id = ?", roomID).Order("id DESC").Limit(limit).Find(&rows).Error; err != nil {
return nil, err
}
out := make([]*biz.Message, 0, len(rows))
for i := len(rows) - 1; i >= 0; i-- {
out = append(out, toBiz(&rows[i]))
}
return out, nil
}
func (r *repo) CreateChat(ctx context.Context, roomID, userID uint, content string) (*biz.Message, error) {
if err := r.ensureRoom(ctx, roomID); err != nil {
return nil, err
}
user, err := r.user(ctx, userID)
if err != nil {
return nil, err
}
row := &messagePO{RoomID: roomID, Type: biz.TypeChat, UserID: user.ID, Username: user.Username, Nickname: user.Nickname, Avatar: user.Avatar, Content: content}
if err = r.createAndPrune(ctx, row); err != nil {
return nil, err
}
return toBiz(row), nil
}
func (r *repo) CreateGift(ctx context.Context, gift *biz.GiftSnapshot) (*biz.Message, error) {
if err := r.ensureRoom(ctx, gift.RoomID); err != nil {
return nil, err
}
user, err := r.user(ctx, gift.UserID)
if err != nil {
return nil, err
}
username, nickname := gift.Username, gift.Nickname
if username == "" {
username = user.Username
}
if nickname == "" {
nickname = user.Nickname
}
row := &messagePO{RoomID: gift.RoomID, Type: biz.TypeGift, UserID: gift.UserID, Username: username, Nickname: nickname, Avatar: user.Avatar, GiftID: gift.GiftID, GiftName: gift.GiftName, GiftIcon: gift.GiftIcon, Quantity: gift.Quantity, DiamondTotal: gift.DiamondTotal}
if err = r.createAndPrune(ctx, row); err != nil {
return nil, err
}
return toBiz(row), nil
}
func (r *repo) createAndPrune(ctx context.Context, row *messagePO) error {
return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Create(row).Error; err != nil {
return err
}
var boundary messagePO
err := tx.Select("id").Where("room_id = ?", row.RoomID).Order("id DESC").Offset(99).Limit(1).Take(&boundary).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
if err != nil {
return err
}
return tx.Where("room_id = ? AND id < ?", row.RoomID, boundary.ID).Delete(&messagePO{}).Error
})
}
func (r *repo) ensureRoom(ctx context.Context, roomID uint) error {
var count int64
if err := r.data.DB().WithContext(ctx).Table("live_rooms").Where("id = ? AND deleted_at IS NULL AND enabled = ?", roomID, true).Count(&count).Error; err != nil {
return err
}
if count == 0 {
return biz.ErrRoomNotFound
}
return nil
}
func (r *repo) user(ctx context.Context, userID uint) (*userSnapshot, error) {
var row userSnapshot
if err := r.data.DB().WithContext(ctx).Table("sys_users").Select("id, username, nick_name AS nickname, header_img AS avatar").Where("id = ? AND deleted_at IS NULL AND enable = ?", userID, 1).Scan(&row).Error; err != nil {
return nil, err
}
if row.ID == 0 {
return nil, biz.ErrUserNotFound
}
return &row, nil
}
func toBiz(row *messagePO) *biz.Message {
return &biz.Message{ID: row.ID, RoomID: row.RoomID, Type: row.Type, UserID: row.UserID, Username: row.Username, Nickname: row.Nickname, Avatar: row.Avatar, Content: row.Content, GiftID: row.GiftID, GiftName: row.GiftName, GiftIcon: row.GiftIcon, Quantity: row.Quantity, DiamondTotal: row.DiamondTotal, CreatedAt: row.CreatedAt}
}
+67
View File
@@ -0,0 +1,67 @@
package livechat
import (
"context"
"fmt"
"testing"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
biz "kra/internal/biz/livechat"
)
type testProvider struct{ db *gorm.DB }
func (p testProvider) DB() *gorm.DB { return p.db }
func openChatTestDB(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(&messagePO{}); err != nil {
t.Fatal(err)
}
for _, statement := range []string{
`CREATE TABLE live_rooms (id integer primary key, enabled numeric, deleted_at datetime)`,
`CREATE TABLE sys_users (id integer primary key, username text, nick_name text, header_img text, enable integer, deleted_at datetime)`,
`INSERT INTO live_rooms(id,enabled) VALUES (7,1)`,
`INSERT INTO sys_users(id,username,nick_name,header_img,enable) VALUES (3,'viewer','Viewer','avatar.png',1)`,
} {
if err = db.Exec(statement).Error; err != nil {
t.Fatal(err)
}
}
return db
}
func TestChatKeepsLatestHundredMessages(t *testing.T) {
db := openChatTestDB(t)
repo := NewRepo(testProvider{db: db})
for index := 1; index <= 105; index++ {
if _, err := repo.CreateChat(context.Background(), 7, 3, fmt.Sprintf("message-%03d", index)); err != nil {
t.Fatal(err)
}
}
items, err := repo.ListRecent(context.Background(), 7, 100)
if err != nil {
t.Fatal(err)
}
if len(items) != 100 || items[0].Content != "message-006" || items[99].Content != "message-105" {
t.Fatalf("history length=%d first=%q last=%q", len(items), items[0].Content, items[len(items)-1].Content)
}
}
func TestGiftMessageUsesSnapshotAndUserAvatar(t *testing.T) {
db := openChatTestDB(t)
repo := NewRepo(testProvider{db: db})
item, err := repo.CreateGift(context.Background(), &biz.GiftSnapshot{RoomID: 7, UserID: 3, Username: "viewer", Nickname: "Viewer", GiftID: 8, GiftName: "Star", GiftIcon: "star.png", Quantity: 10, DiamondTotal: 50})
if err != nil {
t.Fatal(err)
}
if item.Type != biz.TypeGift || item.Avatar != "avatar.png" || item.GiftName != "Star" || item.Quantity != 10 || item.DiamondTotal != 50 {
t.Fatalf("gift message = %+v", item)
}
}
+12
View File
@@ -0,0 +1,12 @@
package livechat
import (
"gorm.io/gorm"
"kra/pkg/database/migration"
)
func Migrations() []migration.Step {
return []migration.Step{{ID: "202609190007_live_chat_schema", Migrate: func(db *gorm.DB) error {
return migration.CreateMissingTables(db, &messagePO{})
}}}
}
+5
View File
@@ -0,0 +1,5 @@
package livechat
import "github.com/google/wire"
var ProviderSet = wire.NewSet(NewRepo)