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
+144
View File
@@ -0,0 +1,144 @@
package asset
import (
"context"
"gorm.io/gorm"
"gorm.io/gorm/clause"
biz "kra/internal/biz/asset"
"kra/internal/data/ledger"
"time"
)
type Provider interface{ DB() *gorm.DB }
type repo struct{ data Provider }
func NewRepo(data Provider) biz.Repo { return &repo{data: data} }
func (r *repo) db(ctx context.Context) *gorm.DB { return r.data.DB().WithContext(ctx) }
type row struct {
UserID uint `gorm:"column:user_id"`
Username string
Nickname string
Avatar string
Enable int
CreatedAt time.Time
DiamondBalance, TotalRechargeCents, TotalSpentDiamonds int64
LastLoginAt *time.Time
}
func query(db *gorm.DB, f biz.Filter) *gorm.DB {
q := db.Table("sys_users").Joins("LEFT JOIN live_wallets ON live_wallets.user_id = sys_users.id").Where("sys_users.deleted_at IS NULL")
if f.Keyword != "" {
v := "%" + f.Keyword + "%"
q = q.Where("sys_users.username LIKE ? OR sys_users.nick_name LIKE ?", v, v)
}
if f.UserID > 0 {
q = q.Where("sys_users.id = ?", f.UserID)
}
if f.Enabled != nil {
if *f.Enabled {
q = q.Where("sys_users.enable = 1")
} else {
q = q.Where("sys_users.enable <> 1")
}
}
if len(f.CreatedAt) == 2 {
q = q.Where("sys_users.created_at BETWEEN ? AND ?", f.CreatedAt[0], f.CreatedAt[1])
}
return q
}
func (r *repo) List(ctx context.Context, f biz.Filter, o biz.ListOptions) ([]*biz.User, int64, error) {
q := query(r.db(ctx), f)
var total int64
if err := q.Count(&total).Error; err != nil {
return nil, 0, err
}
var rows []row
sel := "sys_users.id AS user_id,sys_users.username,sys_users.nick_name AS nickname,sys_users.header_img AS avatar,sys_users.enable,sys_users.created_at,COALESCE(live_wallets.diamond_balance,0) AS diamond_balance,COALESCE(live_wallets.total_recharge_cents,0) AS total_recharge_cents,COALESCE(live_wallets.total_spent_diamonds,0) AS total_spent_diamonds,(SELECT MAX(created_at) FROM sys_login_logs WHERE user_id=sys_users.id AND status=?) AS last_login_at"
if err := q.Select(sel, true).Order(userOrder(o.OrderBy)).Offset(o.Offset).Limit(o.Limit).Scan(&rows).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.User, 0, len(rows))
for i := range rows {
out = append(out, toBiz(&rows[i]))
}
return out, total, nil
}
func (r *repo) Get(ctx context.Context, id uint) (*biz.User, error) {
items, _, err := r.List(ctx, biz.Filter{UserID: id}, biz.ListOptions{Limit: 1})
if err != nil {
return nil, err
}
if len(items) == 0 {
return nil, biz.ErrNotFound
}
v := items[0]
var orders []ledger.RechargeOrderPO
if err := r.db(ctx).Where("user_id = ?", id).Order("created_at DESC").Limit(20).Find(&orders).Error; err != nil {
return nil, err
}
for _, x := range orders {
status := "pending"
if x.CreditedAt != nil {
status = "paid"
}
v.RecentOrders = append(v.RecentOrders, biz.OrderRecord{OrderNo: x.OrderNo, Diamonds: x.Diamonds, AmountCents: x.AmountCents, Status: status, CreatedAt: x.CreatedAt})
}
var sends []ledger.GiftSendPO
if err := r.db(ctx).Where("user_id = ?", id).Order("created_at DESC").Limit(20).Find(&sends).Error; err != nil {
return nil, err
}
for _, x := range sends {
v.RecentSends = append(v.RecentSends, biz.SendRecord{SerialNo: x.SerialNo, RoomName: x.RoomName, GiftName: x.GiftName, GiftIcon: x.GiftIcon, DiamondTotal: x.DiamondTotal, CreatedAt: x.CreatedAt})
}
return v, nil
}
func (r *repo) SetEnabled(ctx context.Context, id uint, enabled bool) error {
value := 2
if enabled {
value = 1
}
res := r.db(ctx).Table("sys_users").Where("id = ? AND deleted_at IS NULL", id).Update("enable", value)
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return biz.ErrNotFound
}
return nil
}
func (r *repo) Adjust(ctx context.Context, v *biz.Adjustment) (*biz.User, error) {
err := r.db(ctx).Transaction(func(tx *gorm.DB) error {
wallet := ledger.WalletPO{UserID: v.UserID}
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("user_id = ?", v.UserID).FirstOrCreate(&wallet).Error; err != nil {
return err
}
balance := wallet.DiamondBalance
if v.Type == "decrease" {
if balance < v.Amount {
return biz.ErrInsufficient
}
balance -= v.Amount
} else {
balance += v.Amount
}
if err := tx.Model(&wallet).Update("diamond_balance", balance).Error; err != nil {
return err
}
return tx.Create(&ledger.BalanceAdjustmentPO{UserID: v.UserID, OperatorUserID: v.OperatorUserID, Type: v.Type, Amount: v.Amount, Reason: v.Reason, BalanceAfter: balance}).Error
})
if err != nil {
return nil, err
}
return r.Get(ctx, v.UserID)
}
func toBiz(v *row) *biz.User {
return &biz.User{UserID: v.UserID, Username: v.Username, Nickname: v.Nickname, Avatar: v.Avatar, Enabled: v.Enable == 1, DiamondBalance: v.DiamondBalance, TotalRechargeCents: v.TotalRechargeCents, TotalSpentDiamonds: v.TotalSpentDiamonds, CreatedAt: v.CreatedAt, LastLoginAt: v.LastLoginAt}
}
func userOrder(v string) string {
m := map[string]string{"id_desc": "sys_users.id DESC", "balance_asc": "diamond_balance ASC", "balance_desc": "diamond_balance DESC", "recharge_asc": "total_recharge_cents ASC", "recharge_desc": "total_recharge_cents DESC", "spent_asc": "total_spent_diamonds ASC", "spent_desc": "total_spent_diamonds DESC", "created_at_asc": "sys_users.created_at ASC", "created_at_desc": "sys_users.created_at DESC", "last_login_asc": "last_login_at ASC", "last_login_desc": "last_login_at DESC"}
if x := m[v]; x != "" {
return x
}
return "sys_users.id ASC"
}
+53
View File
@@ -0,0 +1,53 @@
package asset
import (
"context"
"errors"
"testing"
bizasset "kra/internal/biz/asset"
"kra/internal/data/ledger"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
type testProvider struct{ db *gorm.DB }
func (p testProvider) DB() *gorm.DB { return p.db }
func TestAdjustmentRejectsInsufficientBalanceWithoutAudit(t *testing.T) {
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(&ledger.WalletPO{}, &ledger.BalanceAdjustmentPO{}, &ledger.RechargeOrderPO{}, &ledger.GiftSendPO{}); err != nil {
t.Fatal(err)
}
for _, statement := range []string{
`CREATE TABLE sys_users (id integer primary key, username text, nick_name text, header_img text, enable integer, created_at datetime, deleted_at datetime)`,
`CREATE TABLE sys_login_logs (id integer primary key, user_id integer, status numeric, created_at datetime)`,
} {
if err = db.Exec(statement).Error; err != nil {
t.Fatal(err)
}
}
if err = db.Exec(`INSERT INTO sys_users(id,username,nick_name,enable,created_at) VALUES (4,'user','User',1,CURRENT_TIMESTAMP)`).Error; err != nil {
t.Fatal(err)
}
if err = db.Create(&ledger.WalletPO{UserID: 4, DiamondBalance: 20}).Error; err != nil {
t.Fatal(err)
}
repo := NewRepo(testProvider{db: db})
_, err = repo.Adjust(context.Background(), &bizasset.Adjustment{UserID: 4, OperatorUserID: 1, Type: "decrease", Amount: 30, Reason: "test reason"})
if !errors.Is(err, bizasset.ErrInsufficient) {
t.Fatalf("adjustment error = %v", err)
}
var wallet ledger.WalletPO
var audits int64
db.Where("user_id = ?", 4).First(&wallet)
db.Model(&ledger.BalanceAdjustmentPO{}).Count(&audits)
if wallet.DiamondBalance != 20 || audits != 0 {
t.Fatalf("failed adjustment changed state: wallet=%+v audits=%d", wallet, audits)
}
}
+13
View File
@@ -0,0 +1,13 @@
package asset
import (
"gorm.io/gorm"
"kra/internal/data/ledger"
"kra/pkg/database/migration"
)
func Migrations() []migration.Step {
return []migration.Step{{ID: "202609190004_asset_schema", Migrate: func(db *gorm.DB) error {
return migration.CreateMissingTables(db, &ledger.WalletPO{}, &ledger.BalanceAdjustmentPO{})
}}}
}
+5
View File
@@ -0,0 +1,5 @@
package asset
import "github.com/google/wire"
var ProviderSet = wire.NewSet(NewRepo)