feat: integrate OA activation workflows and business migration
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.NewOtherSettingsUsecase, business.NewTeamUsecase, business.NewGoalUsecase, business.NewEffectivenessUsecase, business.NewPerformanceUsecase, business.NewDashboardUsecase, business.NewAccountUsecase, business.NewBindingUsecase, business.NewPasswordResetUsecase, business.NewBusinessPromoteUsecase, business.NewAlertGovernanceUsecase, business.NewAlertRuleUsecase, business.NewAlertRecordUsecase, business.NewActivationUsecase, business.NewChannelUsecase, business.NewBusinessPortraitUsecase, business.NewChannelPortraitUsecase, business.NewLedgerUsecase, business.NewAnalyticsUsecase, business.NewServerAnalysisUsecase, business.NewWorkbenchUsecase, business.NewRankingUsecase, business.NewReportBuildUsecase, business.NewAlertTriggerUsecase)
|
||||
var ProviderSet = wire.NewSet(system.ProviderSet, payment.ProviderSet, integration.ProviderSet, task.ProviderSet, business.NewUserPreferenceUsecase, business.NewOtherSettingsUsecase, business.NewTeamUsecase, business.NewGoalUsecase, business.NewEffectivenessUsecase, business.NewPerformanceUsecase, business.NewDashboardUsecase, business.NewAccountUsecase, business.NewBindingUsecase, business.NewPasswordResetUsecase, business.NewBusinessPromoteUsecase, business.NewAlertGovernanceUsecase, business.NewAlertRuleUsecase, business.NewAlertRecordUsecase, business.NewActivationUsecase, business.NewActivationPoolUsecase, business.NewChannelUsecase, business.NewBusinessPortraitUsecase, business.NewChannelPortraitUsecase, business.NewLedgerUsecase, business.NewAnalyticsUsecase, business.NewServerAnalysisUsecase, business.NewWorkbenchUsecase, business.NewRankingUsecase, business.NewReportBuildUsecase, business.NewAlertTriggerUsecase)
|
||||
|
||||
@@ -19,17 +19,26 @@ var (
|
||||
)
|
||||
|
||||
type Activation struct {
|
||||
BusierID uint64
|
||||
CreatedAt *time.Time
|
||||
RegisteredPlayers *int64
|
||||
Channels []ChannelKey
|
||||
MetricsAvailable bool
|
||||
ID uint64
|
||||
PromoteID *uint64
|
||||
PlatformName, PromoteAccount, BusierAccount, ApplicantOA, Status, AccumAmount string
|
||||
BindAt, UnbindAt, ReachAt *time.Time
|
||||
}
|
||||
|
||||
func (a *Activation) EndAt() *time.Time {
|
||||
if a.Status == "reached" && a.ReachAt != nil {
|
||||
return a.ReachAt
|
||||
// 达标仍处于绑定关系内,流水和注册统计持续到实际解绑。
|
||||
func (a *Activation) EndAt() *time.Time { return a.UnbindAt }
|
||||
|
||||
// 换绑即结束原关系,即使 OA 定时扫描还没执行,也不能读取新归属期间的流水。
|
||||
func (a *Activation) EndOnTransfer(at time.Time) {
|
||||
if a.BindAt != nil && !at.Before(*a.BindAt) && (a.UnbindAt == nil || at.Before(*a.UnbindAt)) {
|
||||
a.UnbindAt = &at
|
||||
a.Status = "unbound"
|
||||
}
|
||||
return a.UnbindAt
|
||||
}
|
||||
|
||||
type ActivationAccount struct {
|
||||
@@ -41,6 +50,7 @@ type ActivationScope struct {
|
||||
Accounts []ActivationAccount
|
||||
}
|
||||
type ActivationFilter struct {
|
||||
IDCardDedup bool
|
||||
PlatformNames, Statuses []string
|
||||
PromoteAccount, BusierAccount string
|
||||
}
|
||||
@@ -50,12 +60,13 @@ type ActivationPage struct {
|
||||
Available bool
|
||||
}
|
||||
type ActivationSpendQuery struct {
|
||||
Channels []ChannelKey
|
||||
PlatformName string
|
||||
PromoteID uint64
|
||||
Start int64
|
||||
End *int64
|
||||
}
|
||||
type ActivationSpendItem struct{ GameName, Amount string }
|
||||
type ActivationSpendItem struct{ PlatformName, GameName, Amount string }
|
||||
type ActivationSpendDetail struct {
|
||||
Activation *Activation
|
||||
Items []*ActivationSpendItem
|
||||
@@ -70,12 +81,13 @@ type ActivationRepo interface {
|
||||
}
|
||||
|
||||
type ActivationUsecase struct {
|
||||
repo ActivationRepo
|
||||
accounts *AccountUsecase
|
||||
repo ActivationRepo
|
||||
accounts *AccountUsecase
|
||||
preferences *UserPreferenceUsecase
|
||||
}
|
||||
|
||||
func NewActivationUsecase(repo ActivationRepo, accounts *AccountUsecase) *ActivationUsecase {
|
||||
return &ActivationUsecase{repo: repo, accounts: accounts}
|
||||
func NewActivationUsecase(repo ActivationRepo, accounts *AccountUsecase, preferences *UserPreferenceUsecase) *ActivationUsecase {
|
||||
return &ActivationUsecase{repo: repo, accounts: accounts, preferences: preferences}
|
||||
}
|
||||
|
||||
func (uc *ActivationUsecase) scope(ctx context.Context) (ActivationScope, error) {
|
||||
@@ -126,6 +138,11 @@ func (uc *ActivationUsecase) List(ctx context.Context, filter ActivationFilter,
|
||||
for i, status := range filter.Statuses {
|
||||
filter.Statuses[i] = NormalizeActivationStatus(status)
|
||||
}
|
||||
pref, err := uc.preferences.Current(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filter.IDCardDedup = pref.IDCardDedup
|
||||
return uc.repo.List(ctx, scope, filter, options...)
|
||||
}
|
||||
|
||||
@@ -160,7 +177,7 @@ func (uc *ActivationUsecase) SpendDetail(ctx context.Context, id uint64) (*Activ
|
||||
if strings.TrimSpace(activation.PlatformName) == "" || activation.PromoteID == nil || *activation.PromoteID == 0 {
|
||||
return detail, nil
|
||||
}
|
||||
query := ActivationSpendQuery{PlatformName: activation.PlatformName, PromoteID: *activation.PromoteID, Start: activation.BindAt.Unix()}
|
||||
query := ActivationSpendQuery{Channels: activation.Channels, PlatformName: activation.PlatformName, PromoteID: *activation.PromoteID, Start: activation.BindAt.Unix()}
|
||||
if end := activation.EndAt(); end != nil {
|
||||
n := end.Unix()
|
||||
query.End = &n
|
||||
@@ -185,13 +202,13 @@ func (uc *ActivationUsecase) SpendDetail(ctx context.Context, id uint64) (*Activ
|
||||
|
||||
func NormalizeActivationStatus(status string) string {
|
||||
status = strings.TrimSpace(status)
|
||||
if code, ok := map[string]string{"待审核": "pending", "进行中": "in_progress", "已达标": "reached", "已驳回": "rejected", "已解绑": "unbound"}[status]; ok {
|
||||
if code, ok := map[string]string{"待审核": "pending", "审核中": "pending", "进行中": "in_progress", "已达标": "reached", "已驳回": "rejected", "已解绑": "unbound"}[status]; ok {
|
||||
return code
|
||||
}
|
||||
return status
|
||||
}
|
||||
func ActivationStatusLabel(status string) string {
|
||||
if label, ok := map[string]string{"pending": "待审核", "in_progress": "进行中", "reached": "已达标", "rejected": "已驳回", "unbound": "已解绑"}[status]; ok {
|
||||
if label, ok := map[string]string{"pending": "审核中", "in_progress": "进行中", "reached": "已达标", "rejected": "已驳回", "unbound": "已解绑"}[status]; ok {
|
||||
return label
|
||||
}
|
||||
return status
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ActivationPoolFilter struct {
|
||||
Sources []string
|
||||
Name, ChannelType, Status, Target string
|
||||
Begin, End *time.Time
|
||||
}
|
||||
type ActivationPoolChannel struct {
|
||||
ID, PromoteID uint64
|
||||
SourceDB, Name, ChannelType, Status string
|
||||
NoLoginDays *int64
|
||||
EnteredAt time.Time
|
||||
Rejected bool
|
||||
RejectReason string
|
||||
RejectedAt *time.Time
|
||||
}
|
||||
type ActivationPoolPage struct {
|
||||
Rows []*ActivationPoolChannel
|
||||
Total int64
|
||||
Available bool
|
||||
}
|
||||
type ActivationBindingOption struct {
|
||||
SourceDB, Account string
|
||||
ID uint64
|
||||
Level int
|
||||
}
|
||||
type ActivationApplication struct {
|
||||
UserID uint
|
||||
PoolID, BusierID uint64
|
||||
SourceDB string
|
||||
}
|
||||
type ActivationChurn struct {
|
||||
ID, PromoteID uint64
|
||||
SourceDB, Name, From, To, Reason, Operator string
|
||||
At time.Time
|
||||
ToPool bool
|
||||
}
|
||||
type ActivationChurnPage struct {
|
||||
Rows []*ActivationChurn
|
||||
Total int64
|
||||
Available bool
|
||||
}
|
||||
type ActivationPoolRepo interface {
|
||||
Pool(context.Context, ActivationPoolFilter, ...ListOption) (*ActivationPoolPage, error)
|
||||
PoolChannel(context.Context, uint64) (*ActivationPoolChannel, error)
|
||||
BindingOptions(context.Context, uint) ([]*ActivationBindingOption, error)
|
||||
Churn(context.Context, ActivationPoolFilter, ...ListOption) (*ActivationChurnPage, error)
|
||||
}
|
||||
type ActivationGateway interface {
|
||||
Apply(context.Context, ActivationApplication) error
|
||||
}
|
||||
type ActivationPoolUsecase struct {
|
||||
repo ActivationPoolRepo
|
||||
gateway ActivationGateway
|
||||
accounts *AccountUsecase
|
||||
}
|
||||
|
||||
func NewActivationPoolUsecase(repo ActivationPoolRepo, gateway ActivationGateway, accounts *AccountUsecase) *ActivationPoolUsecase {
|
||||
return &ActivationPoolUsecase{repo: repo, gateway: gateway, accounts: accounts}
|
||||
}
|
||||
func (uc *ActivationPoolUsecase) Pool(ctx context.Context, f ActivationPoolFilter, o ...ListOption) (*ActivationPoolPage, error) {
|
||||
if _, _, err := uc.accounts.actor(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return uc.repo.Pool(ctx, f, o...)
|
||||
}
|
||||
func (uc *ActivationPoolUsecase) Options(ctx context.Context) ([]*ActivationBindingOption, error) {
|
||||
a, _, err := uc.accounts.actor(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return uc.repo.BindingOptions(ctx, a.UserID)
|
||||
}
|
||||
func (uc *ActivationPoolUsecase) Apply(ctx context.Context, poolID, busierID uint64, source string) error {
|
||||
a, _, err := uc.accounts.actor(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
source, err = NormalizePlatform(source)
|
||||
if err != nil || poolID == 0 || busierID == 0 {
|
||||
return ErrActivationInput
|
||||
}
|
||||
options, err := uc.repo.BindingOptions(ctx, a.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
owned := false
|
||||
for _, o := range options {
|
||||
if o.SourceDB == source && o.ID == busierID {
|
||||
owned = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !owned {
|
||||
return ErrActivationScope
|
||||
}
|
||||
channel, err := uc.repo.PoolChannel(ctx, poolID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !strings.EqualFold(channel.SourceDB, source) {
|
||||
return ErrActivationInput
|
||||
}
|
||||
return uc.gateway.Apply(ctx, ActivationApplication{UserID: a.UserID, PoolID: poolID, BusierID: busierID, SourceDB: source})
|
||||
}
|
||||
func (uc *ActivationPoolUsecase) Churn(ctx context.Context, f ActivationPoolFilter, o ...ListOption) (*ActivationChurnPage, error) {
|
||||
if _, _, err := uc.accounts.actor(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return uc.repo.Churn(ctx, f, o...)
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type activationPoolFake struct {
|
||||
ActivationPoolRepo
|
||||
options []*ActivationBindingOption
|
||||
requested uint
|
||||
}
|
||||
|
||||
func (r *activationPoolFake) BindingOptions(_ context.Context, id uint) ([]*ActivationBindingOption, error) {
|
||||
r.requested = id
|
||||
return r.options, nil
|
||||
}
|
||||
func (r *activationPoolFake) PoolChannel(context.Context, uint64) (*ActivationPoolChannel, error) {
|
||||
return &ActivationPoolChannel{ID: 10, SourceDB: "lx"}, nil
|
||||
}
|
||||
|
||||
type activationGatewayFake struct {
|
||||
calls int
|
||||
request ActivationApplication
|
||||
}
|
||||
|
||||
func (g *activationGatewayFake) Apply(_ context.Context, a ActivationApplication) error {
|
||||
g.calls++
|
||||
g.request = a
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestActivationApplicationUsesAuthenticatedIdentityAndOwnedBinding(t *testing.T) {
|
||||
accountUC, _ := activationFixture()
|
||||
repo := &activationPoolFake{options: []*ActivationBindingOption{{ID: 9, SourceDB: "lx", Account: "own"}}}
|
||||
gateway := &activationGatewayFake{}
|
||||
uc := NewActivationPoolUsecase(repo, gateway, accountUC.accounts)
|
||||
ctx := accountContext(8, 902)
|
||||
if err := uc.Apply(ctx, 10, 9, "lx"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gateway.request.UserID != 8 || repo.requested != 8 || gateway.request.BusierID != 9 {
|
||||
t.Fatal("applicant came from request rather than session")
|
||||
}
|
||||
for _, a := range []ActivationApplication{{PoolID: 10, BusierID: 99, SourceDB: "lx"}, {PoolID: 10, BusierID: 9, SourceDB: "hw"}, {PoolID: 0, BusierID: 9, SourceDB: "lx"}} {
|
||||
if err := uc.Apply(ctx, a.PoolID, a.BusierID, a.SourceDB); err == nil {
|
||||
t.Fatal("unowned application accepted")
|
||||
}
|
||||
}
|
||||
if err := uc.Apply(context.Background(), 10, 9, "lx"); err == nil {
|
||||
t.Fatal("anonymous request accepted")
|
||||
}
|
||||
if gateway.calls != 1 {
|
||||
t.Fatal("invalid requests reached OA")
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ func (r *activationRepoFake) Spend(_ context.Context, q ActivationSpendQuery) ([
|
||||
func activationFixture() (*ActivationUsecase, *activationRepoFake) {
|
||||
a := &accountRepoFake{accounts: map[uint]*Account{1: {ID: 1, Level: 0}, 7: {ID: 7, Level: 1}, 8: {ID: 8, Level: 2, IDPath: "0,7"}}}
|
||||
r := &activationRepoFake{accounts: []ActivationAccount{{"lx", " Own "}, {"LX", "own"}, {"hw", "Other"}, {"lx", ""}, {"unknown", "Own"}}}
|
||||
return NewActivationUsecase(r, NewAccountUsecase(a)), r
|
||||
return NewActivationUsecase(r, NewAccountUsecase(a), NewUserPreferenceUsecase(&preferenceRepoFake{})), r
|
||||
}
|
||||
func TestActivationScopeIncludesOwnAccountsOnly(t *testing.T) {
|
||||
uc, r := activationFixture()
|
||||
@@ -62,8 +62,8 @@ func TestActivationSpendScopeCutoffAndExactMoney(t *testing.T) {
|
||||
r.record = &Activation{ID: 10, PromoteID: &id, PlatformName: "lx", BusierAccount: "OWN", BindAt: &start, ReachAt: &reach, UnbindAt: &unbind, Status: "reached"}
|
||||
r.items = []*ActivationSpendItem{{Amount: "9007199254740993.01"}, {Amount: "0.09"}}
|
||||
detail, err := uc.SpendDetail(accountContext(8, 902), 10)
|
||||
if err != nil || detail.TotalAmount != "9007199254740993.10" || r.query.Start != start.Unix() || r.query.End == nil || *r.query.End != reach.Unix() {
|
||||
t.Fatalf("达标截止或精确金额错误: %+v %+v %v", detail, r.query, err)
|
||||
if err != nil || detail.TotalAmount != "9007199254740993.10" || r.query.Start != start.Unix() || r.query.End == nil || *r.query.End != unbind.Unix() {
|
||||
t.Fatalf("绑定截止或精确金额错误: %+v %+v %v", detail, r.query, err)
|
||||
}
|
||||
r.record.Status = "unbound"
|
||||
if _, err := uc.SpendDetail(accountContext(8, 902), 10); err != nil || *r.query.End != unbind.Unix() {
|
||||
|
||||
@@ -13,6 +13,7 @@ type DashboardMetrics struct {
|
||||
Pay string
|
||||
}
|
||||
type DashboardData struct {
|
||||
EventsIncomplete bool
|
||||
Current, Previous DashboardMetrics
|
||||
Available bool
|
||||
EventsAvailable bool
|
||||
@@ -84,8 +85,7 @@ func (uc *DashboardUsecase) Overview(ctx context.Context, sources []string, peri
|
||||
}
|
||||
r.Cards[card.key] = value
|
||||
}
|
||||
// The imported SDK channel table has only current state; historical event
|
||||
// counts must stay unavailable until a timestamped event source is connected.
|
||||
// OA 在渠道更新事务中记录事件;按事件发生时间统计,不读取当前锁定状态。
|
||||
for _, card := range []struct {
|
||||
key string
|
||||
current, previous int64
|
||||
@@ -93,6 +93,9 @@ func (uc *DashboardUsecase) Overview(ctx context.Context, sources []string, peri
|
||||
label := "较上一同期"
|
||||
if !data.EventsAvailable {
|
||||
label = "事件数据未接入"
|
||||
if data.EventsIncomplete {
|
||||
label = "所选区间事件历史不完整"
|
||||
}
|
||||
}
|
||||
value, err := workbenchCard(num(card.current), num(card.previous), label, data.Available && data.EventsAvailable)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user