feat: 迁移游戏商务绑定审核流程和列表

This commit is contained in:
Yvan
2026-11-03 18:26:26 +08:00
parent 43f0ec1b15
commit c0a3692068
31 changed files with 1304 additions and 30 deletions
+5 -10
View File
@@ -153,7 +153,7 @@ func checkAccountUnique(tx *gorm.DB, a *business.Account) error {
}
func (r *accountRepo) Create(ctx context.Context, a *business.Account) error {
return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := checkAccountUnique(tx, a); err != nil {
return err
}
@@ -171,10 +171,11 @@ func (r *accountRepo) Create(ctx context.Context, a *business.Account) error {
a.ID, a.CreatedAt, a.UpdatedAt = po.ID, po.CreatedAt, po.UpdatedAt
return nil
})
return storageError(err)
}
func (r *accountRepo) Update(ctx context.Context, a *business.Account) error {
return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := checkAccountUnique(tx, a); err != nil {
return err
}
@@ -194,6 +195,7 @@ func (r *accountRepo) Update(ctx context.Context, a *business.Account) error {
}
return nil
})
return storageError(err)
}
func (r *accountRepo) AuthorityForLevel(ctx context.Context, level int) (uint, error) {
@@ -250,17 +252,10 @@ func (r *accountRepo) DirectCounts(ctx context.Context, ids []uint) (map[uint]bu
result[b.UserID] = business.AccountCounts{PromoteCount: &zero, BindCount: b.Total}
}
// 系统数据演练库尚未包含渠道表时,基础资料与已通过绑定数仍可使用。
tables, err := db.Migrator().GetTables()
available, err := businessTableAvailable(db, "tab_promote")
if err != nil {
return nil, business.ErrStorage
}
available := false
for _, table := range tables {
if table == "tab_promote" {
available = true
break
}
}
if !available {
for _, b := range binds {
value := result[b.UserID]
+161
View File
@@ -0,0 +1,161 @@
package business
import (
"context"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"kra/internal/biz/business"
)
type bindingLockPO struct {
SourceDB string `gorm:"column:source_db;primaryKey;size:50"`
PlatformBusierID uint64 `gorm:"primaryKey;autoIncrement:false"`
}
func (bindingLockPO) TableName() string { return "sys_business_binding_locks" }
type bindingRepo struct{ data Provider }
func NewBindingRepo(data Provider) business.BindingRepo { return &bindingRepo{data: data} }
type bindingTxKey struct{}
func (r *bindingRepo) db(ctx context.Context) *gorm.DB {
if tx, ok := ctx.Value(bindingTxKey{}).(*gorm.DB); ok {
return tx.WithContext(ctx)
}
return r.data.DB().WithContext(ctx)
}
func (r *bindingRepo) WithPlatformLock(ctx context.Context, key business.PlatformKey, fn func(context.Context) error) error {
err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// 单独的键锁覆盖“尚无绑定记录”的并发场景,不修改平台数据库。
lock := bindingLockPO{SourceDB: key.SourceDB, PlatformBusierID: key.BusierID}
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&lock).Error; err != nil {
return business.ErrStorage
}
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("source_db = ? AND platform_busier_id = ?", key.SourceDB, key.BusierID).First(&lock).Error; err != nil {
return business.ErrStorage
}
return fn(context.WithValue(ctx, bindingTxKey{}, tx))
})
return storageError(err)
}
func toBizBinding(po bindingPO) *business.Binding {
return &business.Binding{UserID: po.UserID, PlatformKey: business.PlatformKey{SourceDB: po.SourceDB, BusierID: po.PlatformBusierID}, Account: po.PlatformBusierAccount,
Status: po.Status, RejectReason: po.RejectReason, CreatedAt: po.CreateTime, ReviewTime: po.ReviewTime, ReviewAccount: po.ReviewAccount}
}
func newBinding(b *business.Binding) bindingPO {
return bindingPO{UserID: b.UserID, SourceDB: b.SourceDB, PlatformBusierID: b.BusierID, PlatformBusierAccount: b.Account, Status: b.Status,
RejectReason: b.RejectReason, CreateTime: b.CreatedAt, ReviewTime: b.ReviewTime, ReviewAccount: b.ReviewAccount}
}
func (r *bindingRepo) ForPlatform(ctx context.Context, key business.PlatformKey) ([]*business.Binding, error) {
var pos []bindingPO
if err := r.db(ctx).Where("source_db = ? AND platform_busier_id = ?", key.SourceDB, key.BusierID).Find(&pos).Error; err != nil {
return nil, business.ErrStorage
}
rows := make([]*business.Binding, 0, len(pos))
for _, po := range pos {
rows = append(rows, toBizBinding(po))
}
return rows, nil
}
func (r *bindingRepo) Save(ctx context.Context, b *business.Binding) error {
po := newBinding(b)
err := r.db(ctx).Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "user_id"}, {Name: "source_db"}, {Name: "platform_busier_id"}},
DoUpdates: clause.AssignmentColumns([]string{"platform_busier_account", "status", "reject_reason", "create_time", "review_time", "review_account"})}).Create(&po).Error
if err != nil {
return business.ErrStorage
}
return nil
}
func (r *bindingRepo) Delete(ctx context.Context, userID uint, key business.PlatformKey) error {
result := r.db(ctx).Where("user_id = ? AND source_db = ? AND platform_busier_id = ?", userID, key.SourceDB, key.BusierID).Delete(&bindingPO{})
if result.Error != nil {
return business.ErrStorage
}
if result.RowsAffected == 0 {
return business.ErrBindingNotFound
}
return nil
}
func (r *bindingRepo) List(ctx context.Context, scope business.UserScope, q business.BindingFilter, options ...business.ListOption) ([]*business.Binding, int64, error) {
db := r.db(ctx).Table("sys_user_bind_platform b").Joins("JOIN sys_users u ON u.id=b.user_id AND u.deleted_at IS NULL")
db = applyUserScope(db, scope, "u.")
if q.UserID != 0 {
db = db.Where("b.user_id = ?", q.UserID)
}
if len(q.SourceDBs) > 0 {
db = db.Where("b.source_db IN ?", q.SourceDBs)
}
if q.Account != "" {
db = db.Where("b.platform_busier_account LIKE ?", "%"+q.Account+"%")
}
if q.Status != "" {
db = db.Where("b.status = ?", q.Status)
}
var total int64
if err := db.Count(&total).Error; err != nil {
return nil, 0, business.ErrStorage
}
o := business.NewListOptions(options...)
var pos []struct {
Binding bindingPO `gorm:"embedded"`
UserName string
}
if err := db.Select("b.*,u.username AS user_name").Order("b.create_time DESC").Order("b.source_db,b.platform_busier_id,b.user_id").Offset(o.Offset).Limit(o.Limit).Scan(&pos).Error; err != nil {
return nil, 0, business.ErrStorage
}
rows := make([]*business.Binding, 0, len(pos))
keys := make(map[business.PlatformKey]bool)
for _, po := range pos {
b := toBizBinding(po.Binding)
zero := int64(0)
b.PromoteCount = &zero
b.UserName = po.UserName
rows = append(rows, b)
if b.Status == "0" {
keys[b.PlatformKey] = true
}
}
if len(keys) == 0 {
return rows, total, nil
}
available, err := businessTableAvailable(r.db(ctx), "tab_promote")
if err != nil {
return nil, 0, err
}
if !available {
for _, b := range rows {
if b.Status == "0" {
b.PromoteCount = nil
}
}
return rows, total, nil
}
query := r.db(ctx).Table("tab_promote").Where("promote_level = ?", 1)
pairs := r.db(ctx).Session(&gorm.Session{NewDB: true})
for key := range keys {
pairs = pairs.Or("(source_db = ? AND busier_id = ?)", key.SourceDB, key.BusierID)
}
var counts []struct {
SourceDB string `gorm:"column:source_db"`
BusierID uint64
Total int64
}
if err := query.Where(pairs).Select("source_db,busier_id,COUNT(*) AS total").Group("source_db,busier_id").Scan(&counts).Error; err != nil {
return nil, 0, business.ErrStorage
}
byKey := make(map[business.PlatformKey]int64, len(counts))
for _, c := range counts {
byKey[business.PlatformKey{SourceDB: c.SourceDB, BusierID: c.BusierID}] = c.Total
}
for _, b := range rows {
if b.Status == "0" {
count := byKey[b.PlatformKey]
b.PromoteCount = &count
}
}
return rows, total, nil
}
+76
View File
@@ -0,0 +1,76 @@
package business
import (
"context"
"kra/internal/biz/business"
"testing"
"time"
)
func TestBindingRepositoryClearsAuditAndUsesCompositeIdentity(t *testing.T) {
db := prepareAccounts(t)
r := NewBindingRepo(testData{db})
ctx := context.Background()
reason, review := "旧驳回", "审核员"
now := time.Now()
key := business.PlatformKey{SourceDB: "lx", BusierID: 7}
if err := r.Save(ctx, &business.Binding{UserID: 3, PlatformKey: key, Status: "2", RejectReason: &reason, ReviewTime: &now, ReviewAccount: &review}); err != nil {
t.Fatal(err)
}
if err := r.Save(ctx, &business.Binding{UserID: 3, PlatformKey: business.PlatformKey{SourceDB: "hw", BusierID: 7}, Status: "0"}); err != nil {
t.Fatal(err)
}
if err := r.WithPlatformLock(ctx, key, func(txctx context.Context) error {
return r.Save(txctx, &business.Binding{UserID: 3, PlatformKey: key, Status: "3", CreatedAt: now})
}); err != nil {
t.Fatal(err)
}
rows, err := r.ForPlatform(ctx, key)
if err != nil || len(rows) != 1 {
t.Fatalf("查询失败: %+v %v", rows, err)
}
if rows[0].Status != "3" || rows[0].RejectReason != nil || rows[0].ReviewTime != nil || rows[0].ReviewAccount != nil {
t.Fatal("重提未清除旧审核信息")
}
if err := r.Delete(ctx, 4, key); err != business.ErrBindingNotFound {
t.Fatal("不能删除其他用户绑定")
}
if err := r.Delete(ctx, 3, key); err != nil {
t.Fatal(err)
}
other, err := r.ForPlatform(ctx, business.PlatformKey{SourceDB: "hw", BusierID: 7})
if err != nil || len(other) != 1 {
t.Fatal("解绑影响了同 ID 的其他平台")
}
}
func TestBindingListLoadsDTOFieldsAndSurvivesMissingChannels(t *testing.T) {
db := prepareAccounts(t)
if err := db.Create(&accountPO{ID: 3, Username: "child", Level: 3, IDPath: "0,1,2"}).Error; err != nil {
t.Fatal(err)
}
r := NewBindingRepo(testData{db})
ctx := context.Background()
for _, source := range []string{"lx", "hw"} {
if err := r.Save(ctx, &business.Binding{UserID: 3, PlatformKey: business.PlatformKey{SourceDB: source, BusierID: 7}, Account: "sdk", Status: "0", CreatedAt: time.Now()}); err != nil {
t.Fatal(err)
}
}
if err := db.Exec("INSERT INTO tab_promote VALUES(1,'lx',7,1,1),(2,'lx',7,2,1)").Error; err != nil {
t.Fatal(err)
}
rows, total, err := r.List(ctx, business.UserScope{OnlySelf: true, SelfID: 3}, business.BindingFilter{SourceDBs: []string{"lx"}})
if err != nil || total != 1 || len(rows) != 1 {
t.Fatalf("绑定列表错误: %+v %d %v", rows, total, err)
}
if rows[0].UserID != 3 || rows[0].UserName != "child" || rows[0].Account != "sdk" || rows[0].PromoteCount == nil || *rows[0].PromoteCount != 1 {
t.Fatalf("绑定字段或渠道统计错误: %+v", rows[0])
}
if err := db.Exec("DROP TABLE tab_promote").Error; err != nil {
t.Fatal(err)
}
rows, total, err = r.List(ctx, business.UserScope{OnlySelf: true, SelfID: 3}, business.BindingFilter{})
if err != nil || total != 2 || len(rows) != 2 || rows[0].PromoteCount != nil {
t.Fatalf("缺渠道表不应阻断绑定资料: %+v %d %v", rows, total, err)
}
}
+3 -1
View File
@@ -6,7 +6,9 @@ import (
)
func Migrations() []migration.Step {
return []migration.Step{{ID: "202611030003_business_bindings", Migrate: func(db *gorm.DB) error {
return []migration.Step{{ID: "202611030004_business_binding_locks", Migrate: func(db *gorm.DB) error {
return migration.CreateMissingTables(db, &bindingLockPO{})
}}, {ID: "202611030003_business_bindings", Migrate: func(db *gorm.DB) error {
return migration.CreateMissingTables(db, &bindingPO{})
}}, {ID: "202611030001_business_preference", Migrate: func(db *gorm.DB) error {
// 旧表沿用;已迁移数据库不重建或重置用户偏好。
+45
View File
@@ -0,0 +1,45 @@
package business
import (
"context"
"errors"
"gorm.io/gorm"
"kra/internal/biz/business"
)
type PlatformProvider interface {
Database(string) (*gorm.DB, error)
}
type platformAccountRepo struct{ data PlatformProvider }
func NewPlatformAccountRepo(data PlatformProvider) business.PlatformAccountRepo {
return &platformAccountRepo{data: data}
}
type platformAccountPO struct {
ID uint64
Account, Password string
Status int
}
func (platformAccountPO) TableName() string { return "tab_promote_business" }
func (r *platformAccountRepo) FindByAccount(ctx context.Context, source, account string) (*business.PlatformAccount, error) {
source, err := business.NormalizePlatform(source)
if err != nil {
return nil, err
}
db, err := r.data.Database(source)
if err != nil || db == nil {
return nil, business.ErrPlatformUnavailable
}
var po platformAccountPO
err = db.WithContext(ctx).Where("account = ?", account).First(&po).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, business.ErrPlatformUnavailable
}
return &business.PlatformAccount{ID: po.ID, Account: po.Account, Password: po.Password, Status: po.Status}, nil
}
+32
View File
@@ -0,0 +1,32 @@
package business
import (
"errors"
kratoserrors "github.com/go-kratos/kratos/v3/errors"
"gorm.io/gorm"
"kra/internal/biz/business"
)
func storageError(err error) error {
if err == nil {
return nil
}
var typed *kratoserrors.Error
if errors.As(err, &typed) {
return err
}
return business.ErrStorage
}
func businessTableAvailable(db *gorm.DB, name string) (bool, error) {
tables, err := db.Migrator().GetTables()
if err != nil {
return false, business.ErrStorage
}
for _, table := range tables {
if table == name {
return true, nil
}
}
return false, nil
}
+3
View File
@@ -27,8 +27,11 @@ import (
var ProviderSet = wire.NewSet(
NewData,
wire.Bind(new(databusiness.Provider), new(*Data)),
wire.Bind(new(databusiness.PlatformProvider), new(*Data)),
databusiness.NewUserPreferenceRepo,
databusiness.NewAccountRepo,
databusiness.NewBindingRepo,
databusiness.NewPlatformAccountRepo,
NewIntegrationRuntime,
wire.Bind(new(datasystem.Provider), new(*Data)),
wire.Bind(new(datasystem.DatabaseProvider), new(*Data)),
+2 -2
View File
@@ -34,8 +34,8 @@ func TestMigrateAllRunsModuleSchemasWithoutBootstrapSeed(t *testing.T) {
if err = db.Table(migration.TableName).Count(&versions).Error; err != nil {
t.Fatal(err)
}
if versions != 17 {
t.Fatalf("migration versions = %d, want 17", versions)
if versions != 18 {
t.Fatalf("migration versions = %d, want 18", versions)
}
var communicationRows []dataintegration.ConfigPO
if err = db.Where("kind IN ?", []string{"mq", "websocket"}).Order("kind, provider").Find(&communicationRows).Error; err != nil {