feat: live module and jenkins pipeline to harbor deploy
This commit is contained in:
@@ -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"
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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{})
|
||||
}}}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package asset
|
||||
|
||||
import "github.com/google/wire"
|
||||
|
||||
var ProviderSet = wire.NewSet(NewRepo)
|
||||
Reference in New Issue
Block a user