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, business.NewBindingUsecase, business.NewPasswordResetUsecase, business.NewBusinessPromoteUsecase, business.NewAlertRuleUsecase, business.NewAlertRecordUsecase)
|
||||
var ProviderSet = wire.NewSet(system.ProviderSet, payment.ProviderSet, integration.ProviderSet, task.ProviderSet, business.NewUserPreferenceUsecase, business.NewAccountUsecase, business.NewBindingUsecase, business.NewPasswordResetUsecase, business.NewBusinessPromoteUsecase, business.NewAlertRuleUsecase, business.NewAlertRecordUsecase, business.NewActivationUsecase)
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-kratos/kratos/v3/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrActivationNotFound = errors.NotFound("SW_ACTIVATION_NOT_FOUND", "绑定记录不存在")
|
||||
ErrActivationScope = errors.Forbidden("SW_ACTIVATION_SCOPE", "无权查看该绑定记录")
|
||||
ErrActivationPending = errors.BadRequest("SW_ACTIVATION_PENDING", "审核通过后可查看")
|
||||
ErrActivationSource = errors.BadRequest("SW_ACTIVATION_SOURCE", "OA 渠道激活数据源未接入或暂不可用")
|
||||
ErrActivationMetrics = errors.BadRequest("SW_ACTIVATION_METRICS", "渠道或流水数据尚未接入,暂时无法查询流水明细")
|
||||
ErrActivationInput = errors.BadRequest("SW_ACTIVATION_INPUT", "渠道激活查询参数不正确")
|
||||
)
|
||||
|
||||
type Activation struct {
|
||||
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
|
||||
}
|
||||
return a.UnbindAt
|
||||
}
|
||||
|
||||
type ActivationScope struct {
|
||||
All bool
|
||||
Accounts []string
|
||||
}
|
||||
type ActivationFilter struct {
|
||||
PlatformNames, Statuses []string
|
||||
PromoteAccount, BusierAccount string
|
||||
}
|
||||
type ActivationPage struct {
|
||||
Rows []*Activation
|
||||
Total int64
|
||||
Available bool
|
||||
}
|
||||
type ActivationSpendQuery struct {
|
||||
PlatformName string
|
||||
PromoteID uint64
|
||||
Start int64
|
||||
End *int64
|
||||
}
|
||||
type ActivationSpendItem struct{ GameName, Amount string }
|
||||
type ActivationSpendDetail struct {
|
||||
Activation *Activation
|
||||
Items []*ActivationSpendItem
|
||||
TotalAmount string
|
||||
}
|
||||
|
||||
type ActivationRepo interface {
|
||||
OwnAccounts(context.Context, uint) ([]string, error)
|
||||
List(context.Context, ActivationScope, ActivationFilter, ...ListOption) (*ActivationPage, error)
|
||||
Find(context.Context, uint64) (*Activation, error)
|
||||
Spend(context.Context, ActivationSpendQuery) ([]*ActivationSpendItem, error)
|
||||
}
|
||||
|
||||
type ActivationUsecase struct {
|
||||
repo ActivationRepo
|
||||
accounts *AccountUsecase
|
||||
}
|
||||
|
||||
func NewActivationUsecase(repo ActivationRepo, accounts *AccountUsecase) *ActivationUsecase {
|
||||
return &ActivationUsecase{repo: repo, accounts: accounts}
|
||||
}
|
||||
|
||||
func (uc *ActivationUsecase) scope(ctx context.Context) (ActivationScope, error) {
|
||||
actor, _, err := uc.accounts.actor(ctx)
|
||||
if err != nil {
|
||||
return ActivationScope{}, err
|
||||
}
|
||||
if actor.AuthorityID == 888 {
|
||||
return ActivationScope{All: true}, nil
|
||||
}
|
||||
accounts, err := uc.repo.OwnAccounts(ctx, actor.UserID)
|
||||
if err != nil {
|
||||
return ActivationScope{}, err
|
||||
}
|
||||
scope := ActivationScope{}
|
||||
seen := map[string]bool{}
|
||||
for _, account := range accounts {
|
||||
account = strings.TrimSpace(account)
|
||||
key := strings.ToLower(account)
|
||||
if key != "" && !seen[key] {
|
||||
scope.Accounts = append(scope.Accounts, account)
|
||||
seen[key] = true
|
||||
}
|
||||
}
|
||||
return scope, nil
|
||||
}
|
||||
|
||||
func (uc *ActivationUsecase) List(ctx context.Context, filter ActivationFilter, options ...ListOption) (*ActivationPage, error) {
|
||||
scope, err := uc.scope(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !scope.All && filter.BusierAccount != "" {
|
||||
matched := []string{}
|
||||
keyword := strings.ToLower(strings.TrimSpace(filter.BusierAccount))
|
||||
for _, account := range scope.Accounts {
|
||||
if strings.Contains(strings.ToLower(account), keyword) {
|
||||
matched = append(matched, account)
|
||||
}
|
||||
}
|
||||
scope.Accounts = matched
|
||||
filter.BusierAccount = ""
|
||||
}
|
||||
if !scope.All && len(scope.Accounts) == 0 {
|
||||
return &ActivationPage{Rows: []*Activation{}, Available: true}, nil
|
||||
}
|
||||
for i, status := range filter.Statuses {
|
||||
filter.Statuses[i] = NormalizeActivationStatus(status)
|
||||
}
|
||||
return uc.repo.List(ctx, scope, filter, options...)
|
||||
}
|
||||
|
||||
func (uc *ActivationUsecase) SpendDetail(ctx context.Context, id uint64) (*ActivationSpendDetail, error) {
|
||||
scope, err := uc.scope(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !scope.All && len(scope.Accounts) == 0 {
|
||||
return nil, ErrActivationScope
|
||||
}
|
||||
activation, err := uc.repo.Find(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !scope.All {
|
||||
found := false
|
||||
for _, account := range scope.Accounts {
|
||||
if strings.EqualFold(account, strings.TrimSpace(activation.BusierAccount)) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, ErrActivationScope
|
||||
}
|
||||
}
|
||||
if activation.BindAt == nil {
|
||||
return nil, ErrActivationPending
|
||||
}
|
||||
detail := &ActivationSpendDetail{Activation: activation, Items: []*ActivationSpendItem{}, TotalAmount: "0.00"}
|
||||
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()}
|
||||
if end := activation.EndAt(); end != nil {
|
||||
n := end.Unix()
|
||||
query.End = &n
|
||||
}
|
||||
detail.Items, err = uc.repo.Spend(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 金额合计不用浮点数,避免多个游戏相加产生误差或大金额精度损失。
|
||||
total := new(big.Rat)
|
||||
for _, item := range detail.Items {
|
||||
amount, ok := new(big.Rat).SetString(item.Amount)
|
||||
if !ok {
|
||||
return nil, ErrStorage
|
||||
}
|
||||
item.Amount = amount.FloatString(2)
|
||||
total.Add(total, amount)
|
||||
}
|
||||
detail.TotalAmount = total.FloatString(2)
|
||||
return detail, nil
|
||||
}
|
||||
|
||||
func NormalizeActivationStatus(status string) string {
|
||||
status = strings.TrimSpace(status)
|
||||
if code, ok := map[string]string{"待审核": "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 {
|
||||
return label
|
||||
}
|
||||
return status
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type activationRepoFake struct {
|
||||
ActivationRepo
|
||||
accounts []string
|
||||
requested uint
|
||||
scope ActivationScope
|
||||
filter ActivationFilter
|
||||
record *Activation
|
||||
query ActivationSpendQuery
|
||||
items []*ActivationSpendItem
|
||||
listCalls int
|
||||
}
|
||||
|
||||
func (r *activationRepoFake) OwnAccounts(_ context.Context, id uint) ([]string, error) {
|
||||
r.requested = id
|
||||
return r.accounts, nil
|
||||
}
|
||||
func (r *activationRepoFake) List(_ context.Context, scope ActivationScope, filter ActivationFilter, _ ...ListOption) (*ActivationPage, error) {
|
||||
r.listCalls++
|
||||
r.scope = scope
|
||||
r.filter = filter
|
||||
return &ActivationPage{Rows: []*Activation{}, Available: true}, nil
|
||||
}
|
||||
func (r *activationRepoFake) Find(context.Context, uint64) (*Activation, error) { return r.record, nil }
|
||||
func (r *activationRepoFake) Spend(_ context.Context, q ActivationSpendQuery) ([]*ActivationSpendItem, error) {
|
||||
r.query = q
|
||||
return r.items, nil
|
||||
}
|
||||
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: []string{" Own ", "own", "Other", ""}}
|
||||
return NewActivationUsecase(r, NewAccountUsecase(a)), r
|
||||
}
|
||||
func TestActivationScopeIncludesOwnAccountsOnly(t *testing.T) {
|
||||
uc, r := activationFixture()
|
||||
if _, err := uc.List(accountContext(8, 902), ActivationFilter{BusierAccount: "oWn", Statuses: []string{"已达标"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.requested != 8 || r.scope.All || len(r.scope.Accounts) != 1 || r.scope.Accounts[0] != "Own" || r.filter.BusierAccount != "" || r.filter.Statuses[0] != "reached" {
|
||||
t.Fatalf("本人账号或中文状态筛选错误: %+v", r)
|
||||
}
|
||||
page, err := uc.List(accountContext(7, 901), ActivationFilter{BusierAccount: "下级账号"})
|
||||
if err != nil || page.Total != 0 || r.listCalls != 1 {
|
||||
t.Fatal("无匹配绑定应直接返回空集")
|
||||
}
|
||||
if _, err := uc.List(accountContext(1, 888), ActivationFilter{BusierAccount: "any"}); err != nil || !r.scope.All || r.filter.BusierAccount != "any" {
|
||||
t.Fatal("超管可筛全部 OA 绑定")
|
||||
}
|
||||
}
|
||||
func TestActivationSpendScopeCutoffAndExactMoney(t *testing.T) {
|
||||
uc, r := activationFixture()
|
||||
start := time.Date(2026, 11, 3, 0, 0, 0, 0, time.FixedZone("CST", 8*3600))
|
||||
reach, unbind := start.Add(time.Hour), start.Add(2*time.Hour)
|
||||
id := uint64(100)
|
||||
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)
|
||||
}
|
||||
r.record.Status = "unbound"
|
||||
if _, err := uc.SpendDetail(accountContext(8, 902), 10); err != nil || *r.query.End != unbind.Unix() {
|
||||
t.Fatal("解绑截止错误")
|
||||
}
|
||||
r.record.BusierAccount = "not-owned"
|
||||
if _, err := uc.SpendDetail(accountContext(8, 902), 10); err != ErrActivationScope {
|
||||
t.Fatal("非本人绑定可读取流水")
|
||||
}
|
||||
r.record.BindAt = nil
|
||||
if _, err := uc.SpendDetail(accountContext(1, 888), 10); err != ErrActivationPending {
|
||||
t.Fatal("未审核绑定不应查询流水")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user