feat: 迁移游戏商务绑定审核流程和列表
This commit is contained in:
+1
-1
@@ -11,4 +11,4 @@ import (
|
||||
)
|
||||
|
||||
// ProviderSet is biz providers.
|
||||
var ProviderSet = wire.NewSet(system.ProviderSet, payment.ProviderSet, integration.ProviderSet, task.ProviderSet, business.NewUserPreferenceUsecase, business.NewAccountUsecase)
|
||||
var ProviderSet = wire.NewSet(system.ProviderSet, payment.ProviderSet, integration.ProviderSet, task.ProviderSet, business.NewUserPreferenceUsecase, business.NewAccountUsecase, business.NewBindingUsecase)
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/go-kratos/kratos/v3/errors"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPlatform = errors.BadRequest("SW_PLATFORM_INVALID", "所属平台不正确")
|
||||
ErrPlatformUnavailable = errors.BadRequest("SW_PLATFORM_UNAVAILABLE", "平台数据源未配置或暂不可用")
|
||||
ErrPlatformPasswordConfig = errors.BadRequest("SW_PLATFORM_PASSWORD_CONFIG", "平台密码兼容配置未就绪")
|
||||
ErrPlatformAccount = errors.BadRequest("SW_PLATFORM_ACCOUNT", "游戏商务账号不存在或已禁用")
|
||||
ErrPlatformPassword = errors.BadRequest("SW_PLATFORM_PASSWORD", "游戏商务账号或密码错误")
|
||||
ErrBindingLevel = errors.BadRequest("SW_BINDING_LEVEL", "仅三级商务可绑定游戏 SDK 商务")
|
||||
ErrBindingOccupied = errors.BadRequest("SW_BINDING_OCCUPIED", "该游戏商务已被其他账号绑定或审核中")
|
||||
ErrBindingExists = errors.BadRequest("SW_BINDING_EXISTS", "该游戏商务已绑定")
|
||||
ErrBindingPending = errors.BadRequest("SW_BINDING_PENDING", "该游戏商务已提交,请等待 OA 审核")
|
||||
ErrBindingNotFound = errors.NotFound("SW_BINDING_NOT_FOUND", "绑定记录不存在")
|
||||
)
|
||||
|
||||
type PlatformKey struct {
|
||||
SourceDB string
|
||||
BusierID uint64
|
||||
}
|
||||
type PlatformAccount struct {
|
||||
ID uint64
|
||||
Account, Password string
|
||||
Status int
|
||||
}
|
||||
type PlatformAccountRepo interface {
|
||||
FindByAccount(context.Context, string, string) (*PlatformAccount, error)
|
||||
}
|
||||
type PlatformPasswordVerifier interface {
|
||||
Verify(string, string) (bool, error)
|
||||
}
|
||||
|
||||
func NormalizePlatform(value string) (string, error) {
|
||||
value = strings.ToLower(strings.TrimSpace(value))
|
||||
switch value {
|
||||
case "lx", "hw", "yq":
|
||||
return value, nil
|
||||
default:
|
||||
return "", ErrPlatform
|
||||
}
|
||||
}
|
||||
func PlatformName(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "lx":
|
||||
return "龙翔"
|
||||
case "hw":
|
||||
return "汇玩"
|
||||
case "yq":
|
||||
return "游趣"
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
type Binding struct {
|
||||
UserID uint
|
||||
PlatformKey
|
||||
Account, UserName string
|
||||
Status string
|
||||
RejectReason *string
|
||||
CreatedAt time.Time
|
||||
ReviewTime *time.Time
|
||||
ReviewAccount *string
|
||||
PromoteCount *int64
|
||||
}
|
||||
|
||||
type BindingFilter struct {
|
||||
UserID uint
|
||||
SourceDBs []string
|
||||
Account string
|
||||
Status string
|
||||
}
|
||||
type BindingRepo interface {
|
||||
List(context.Context, UserScope, BindingFilter, ...ListOption) ([]*Binding, int64, error)
|
||||
WithPlatformLock(context.Context, PlatformKey, func(context.Context) error) error
|
||||
ForPlatform(context.Context, PlatformKey) ([]*Binding, error)
|
||||
Save(context.Context, *Binding) error
|
||||
Delete(context.Context, uint, PlatformKey) error
|
||||
}
|
||||
|
||||
type BindingUsecase struct {
|
||||
repo BindingRepo
|
||||
accounts *AccountUsecase
|
||||
platforms PlatformAccountRepo
|
||||
passwords PlatformPasswordVerifier
|
||||
}
|
||||
|
||||
func NewBindingUsecase(repo BindingRepo, accounts *AccountUsecase, platforms PlatformAccountRepo, passwords PlatformPasswordVerifier) *BindingUsecase {
|
||||
return &BindingUsecase{repo: repo, accounts: accounts, platforms: platforms, passwords: passwords}
|
||||
}
|
||||
|
||||
func (uc *BindingUsecase) List(ctx context.Context, q BindingFilter, options ...ListOption) ([]*Binding, int64, error) {
|
||||
actor, current, err := uc.accounts.actor(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
scope := UserScope{All: actor.AuthorityID == 888}
|
||||
if !scope.All {
|
||||
scope.SelfID = current.ID
|
||||
if current.Level >= 3 {
|
||||
scope.OnlySelf = true
|
||||
} else {
|
||||
scope.DescendantPath = descendantPath(current)
|
||||
}
|
||||
}
|
||||
return uc.repo.List(ctx, scope, q, options...)
|
||||
}
|
||||
|
||||
func (uc *BindingUsecase) ForUser(ctx context.Context, id uint) ([]*Binding, error) {
|
||||
if _, err := uc.accounts.Find(ctx, id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, _, err := uc.repo.List(ctx, UserScope{SelfID: id, OnlySelf: true}, BindingFilter{UserID: id, Status: "0"}, ListLimit(-1))
|
||||
return rows, err
|
||||
}
|
||||
|
||||
func (uc *BindingUsecase) Bind(ctx context.Context, source, account, password string) error {
|
||||
_, current, err := uc.accounts.actor(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if current.Level != 3 {
|
||||
return ErrBindingLevel
|
||||
}
|
||||
source, err = NormalizePlatform(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
account = strings.TrimSpace(account)
|
||||
platform, err := uc.platforms.FindByAccount(ctx, source, account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if platform == nil || platform.Status != 1 {
|
||||
return ErrPlatformAccount
|
||||
}
|
||||
valid, err := uc.passwords.Verify(password, platform.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !valid {
|
||||
return ErrPlatformPassword
|
||||
}
|
||||
key := PlatformKey{SourceDB: source, BusierID: platform.ID}
|
||||
// 平台验密完成后才开启主库事务,避免跨库事务切源。
|
||||
return uc.repo.WithPlatformLock(ctx, key, func(txctx context.Context) error {
|
||||
bindings, err := uc.repo.ForPlatform(txctx, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, b := range bindings {
|
||||
if b.UserID != current.ID && (b.Status == "0" || b.Status == "3") {
|
||||
return ErrBindingOccupied
|
||||
}
|
||||
}
|
||||
for _, b := range bindings {
|
||||
if b.UserID == current.ID {
|
||||
if b.Status == "0" {
|
||||
return ErrBindingExists
|
||||
}
|
||||
if b.Status == "3" {
|
||||
return ErrBindingPending
|
||||
}
|
||||
}
|
||||
}
|
||||
return uc.repo.Save(txctx, &Binding{UserID: current.ID, PlatformKey: key, Account: account, Status: "3", CreatedAt: time.Now()})
|
||||
})
|
||||
}
|
||||
|
||||
func (uc *BindingUsecase) Unbind(ctx context.Context, key PlatformKey) error {
|
||||
_, current, err := uc.accounts.actor(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key.SourceDB, err = NormalizePlatform(key.SourceDB)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if key.BusierID == 0 {
|
||||
return ErrBindingNotFound
|
||||
}
|
||||
return uc.repo.WithPlatformLock(ctx, key, func(txctx context.Context) error { return uc.repo.Delete(txctx, current.ID, key) })
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type bindingRepoFake struct {
|
||||
BindingRepo
|
||||
mu sync.Mutex
|
||||
rows []*Binding
|
||||
saved *Binding
|
||||
scope UserScope
|
||||
filter BindingFilter
|
||||
}
|
||||
|
||||
func (r *bindingRepoFake) WithPlatformLock(ctx context.Context, _ PlatformKey, fn func(context.Context) error) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return fn(ctx)
|
||||
}
|
||||
func (r *bindingRepoFake) ForPlatform(context.Context, PlatformKey) ([]*Binding, error) {
|
||||
return r.rows, nil
|
||||
}
|
||||
func (r *bindingRepoFake) Save(_ context.Context, b *Binding) error {
|
||||
r.saved = b
|
||||
r.rows = []*Binding{b}
|
||||
return nil
|
||||
}
|
||||
func (r *bindingRepoFake) List(_ context.Context, s UserScope, q BindingFilter, _ ...ListOption) ([]*Binding, int64, error) {
|
||||
r.scope, r.filter = s, q
|
||||
return []*Binding{}, 0, nil
|
||||
}
|
||||
|
||||
type platformRepoFake struct {
|
||||
account *PlatformAccount
|
||||
calls int
|
||||
}
|
||||
|
||||
func (r *platformRepoFake) FindByAccount(context.Context, string, string) (*PlatformAccount, error) {
|
||||
r.calls++
|
||||
return r.account, nil
|
||||
}
|
||||
|
||||
type passwordFake struct{}
|
||||
|
||||
func (passwordFake) Verify(string, string) (bool, error) { return true, nil }
|
||||
|
||||
func newBindingTestUsecase(repo *bindingRepoFake) (*BindingUsecase, *platformRepoFake) {
|
||||
a := &accountRepoFake{accounts: map[uint]*Account{1: {ID: 1, Level: 1, IDPath: "0"}, 2: {ID: 2, Level: 2, IDPath: "0,1", ParentID: 1}, 3: {ID: 3, Level: 3, IDPath: "0,1,2", ParentID: 2}, 4: {ID: 4, Level: 3, IDPath: "0,1,2", ParentID: 2}}}
|
||||
p := &platformRepoFake{account: &PlatformAccount{ID: 7, Status: 1}}
|
||||
return NewBindingUsecase(repo, NewAccountUsecase(a), p, passwordFake{}), p
|
||||
}
|
||||
|
||||
func TestBindingTransitionsAndLevel(t *testing.T) {
|
||||
for _, item := range []struct {
|
||||
name string
|
||||
existing *Binding
|
||||
want error
|
||||
}{
|
||||
{"新增", nil, nil}, {"重提驳回", &Binding{UserID: 3, Status: "2"}, nil}, {"本人通过", &Binding{UserID: 3, Status: "0"}, ErrBindingExists},
|
||||
{"本人待审核", &Binding{UserID: 3, Status: "3"}, ErrBindingPending}, {"其他账号通过", &Binding{UserID: 4, Status: "0"}, ErrBindingOccupied}, {"其他账号待审核", &Binding{UserID: 4, Status: "3"}, ErrBindingOccupied},
|
||||
} {
|
||||
t.Run(item.name, func(t *testing.T) {
|
||||
r := &bindingRepoFake{}
|
||||
if item.existing != nil {
|
||||
r.rows = []*Binding{item.existing}
|
||||
}
|
||||
uc, _ := newBindingTestUsecase(r)
|
||||
if err := uc.Bind(accountContext(3, 903), "LX", " sdk ", "password"); err != item.want {
|
||||
t.Fatalf("绑定结果错误: %v", err)
|
||||
}
|
||||
if item.want == nil && (r.saved.Status != "3" || r.saved.Account != "sdk" || r.saved.SourceDB != "lx" || r.saved.RejectReason != nil || r.saved.ReviewTime != nil || r.saved.ReviewAccount != nil) {
|
||||
t.Fatalf("待审核状态未正确重置: %+v", r.saved)
|
||||
}
|
||||
})
|
||||
}
|
||||
r := &bindingRepoFake{}
|
||||
uc, p := newBindingTestUsecase(r)
|
||||
if err := uc.Bind(accountContext(2, 902), "lx", "sdk", "password"); err != ErrBindingLevel || p.calls != 0 {
|
||||
t.Fatal("非三级不应访问平台验密")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindingScopesMatchBusinessLevels(t *testing.T) {
|
||||
r := &bindingRepoFake{}
|
||||
uc, _ := newBindingTestUsecase(r)
|
||||
if _, _, err := uc.List(accountContext(1, 901), BindingFilter{}); err != nil || r.scope.IncludeSelf || r.scope.OnlySelf || r.scope.DescendantPath != "0,1" {
|
||||
t.Fatalf("一级仅查看下级绑定: %+v %v", r.scope, err)
|
||||
}
|
||||
if _, _, err := uc.List(accountContext(3, 903), BindingFilter{}); err != nil || !r.scope.OnlySelf || r.scope.SelfID != 3 {
|
||||
t.Fatal("三级只能查看自己的绑定")
|
||||
}
|
||||
if _, err := uc.ForUser(accountContext(1, 901), 3); err != nil || r.filter.Status != "0" || r.filter.UserID != 3 {
|
||||
t.Fatal("绑定详情必须仅已通过")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindingConcurrentSubmissionHasOneOwner(t *testing.T) {
|
||||
r := &bindingRepoFake{}
|
||||
uc, _ := newBindingTestUsecase(r)
|
||||
// 每个用例独立的平台桩避免把桩的计数器带进并发断言。
|
||||
other, _ := newBindingTestUsecase(r)
|
||||
errors := make(chan error, 2)
|
||||
go func() { errors <- uc.Bind(accountContext(3, 903), "lx", "sdk", "password") }()
|
||||
go func() { errors <- other.Bind(accountContext(4, 903), "lx", "sdk", "password") }()
|
||||
first, second := <-errors, <-errors
|
||||
if !((first == nil && second == ErrBindingOccupied) || (second == nil && first == ErrBindingOccupied)) {
|
||||
t.Fatalf("同一平台商务必须独占: %v %v", first, second)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user