375 lines
12 KiB
Go
375 lines
12 KiB
Go
package business
|
||
|
||
import (
|
||
"context"
|
||
"math/big"
|
||
"regexp"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
"unicode"
|
||
"unicode/utf16"
|
||
|
||
"github.com/go-kratos/kratos/v3/errors"
|
||
)
|
||
|
||
var (
|
||
ErrChannelNotFound = errors.NotFound("SW_CHANNEL_NOT_FOUND", "渠道不存在")
|
||
ErrChannelScope = errors.Forbidden("SW_CHANNEL_SCOPE", "无权查看该渠道")
|
||
ErrChannelInput = errors.BadRequest("SW_CHANNEL_INPUT", "渠道查询参数不正确")
|
||
ErrChannelAccount = errors.BadRequest("SW_CHANNEL_ACCOUNT", "渠道账号需为中文/英文/数字,长度 4-30 位")
|
||
ErrChannelPassword = errors.BadRequest("SW_CHANNEL_PASSWORD", "渠道密码长度需为 6-15 位")
|
||
ErrChannelBinding = errors.BadRequest("SW_CHANNEL_BINDING", "请选择有效的游戏商务绑定")
|
||
ErrChannelExists = errors.BadRequest("SW_CHANNEL_EXISTS", "渠道账号已存在")
|
||
ErrChannelUnavailable = errors.BadRequest("SW_CHANNEL_UNAVAILABLE", "渠道数据尚未接入")
|
||
ErrChannelReportsUnavailable = errors.BadRequest("SW_CHANNEL_REPORTS_UNAVAILABLE", "游戏日汇总数据尚未接入")
|
||
ErrChannelBusy = errors.Conflict("SW_CHANNEL_BUSY", "该渠道正在创建,请稍后重试")
|
||
ErrChannelPlatformOwner = errors.BadRequest("SW_CHANNEL_PLATFORM_OWNER", "游戏商务不存在或渠道关系更新失败")
|
||
)
|
||
|
||
type ChannelKey struct {
|
||
SourceDB string
|
||
ID uint64
|
||
}
|
||
type Channel struct {
|
||
ChannelKey
|
||
Account, Nickname, ParentName, RealName string
|
||
Phone string
|
||
SettlementDays *int
|
||
Level, RegisterType, Status, LockStatus *int
|
||
ParentID, TopPromoteID, BusierID uint64
|
||
BusierUserID *uint
|
||
BindingAccount, BindingUserName string
|
||
GameCount, PlayerCount *int64
|
||
TotalPayAmount *string
|
||
CreatedUnix, LastLoginUnix *int64
|
||
QualifiedAt *int64
|
||
}
|
||
|
||
func (c *Channel) BusinessDisplayName() string {
|
||
account, user := strings.TrimSpace(c.BindingAccount), strings.TrimSpace(c.BindingUserName)
|
||
if account == "" {
|
||
return user
|
||
}
|
||
if user == "" || account == user {
|
||
return account
|
||
}
|
||
return account + "(" + user + ")"
|
||
}
|
||
func (c *Channel) IncludesStat(other *Channel, includeSub bool) bool {
|
||
if c.SourceDB != other.SourceDB {
|
||
return false
|
||
}
|
||
if c.ID == other.ID {
|
||
return true
|
||
}
|
||
if !includeSub || c.Level == nil || *c.Level >= 3 {
|
||
return false
|
||
}
|
||
if *c.Level == 2 {
|
||
return other.ParentID == c.ID && other.Level != nil && *other.Level == 3
|
||
}
|
||
return other.ParentID == c.ID || other.TopPromoteID == c.ID
|
||
}
|
||
|
||
type ChannelFilter struct {
|
||
Effective *bool
|
||
Sources []string
|
||
BusierUserID *uint
|
||
BusierAccount, Keyword string
|
||
Phone, SortField string
|
||
SettlementDays *int
|
||
CreatedBegin, CreatedEnd *time.Time
|
||
LoginBegin, LoginEnd *time.Time
|
||
Ascending bool
|
||
Level, RegisterType, Status, LockStatus *int
|
||
}
|
||
type ChannelStatusCounts struct {
|
||
Normal, Locked, Terminated int64
|
||
}
|
||
type ChannelPage struct {
|
||
Rows []*Channel
|
||
Total int64
|
||
Available bool
|
||
StatusCounts *ChannelStatusCounts
|
||
}
|
||
type ChannelPlayer struct {
|
||
ChannelKey
|
||
UserID uint64
|
||
IDCard string
|
||
}
|
||
type ChannelMetricData struct {
|
||
Games map[ChannelKey]int64
|
||
Players map[ChannelKey]int64
|
||
Pay map[ChannelKey]string
|
||
PlayerCards []ChannelPlayer
|
||
GamesAvailable, PlayersAvailable, PayAvailable bool
|
||
}
|
||
type ChannelGame struct {
|
||
ID uint64
|
||
Name string
|
||
RegisterCount int64
|
||
PayAmount string
|
||
}
|
||
type ChannelGameScope struct {
|
||
Root *Channel
|
||
IncludeSub, IDCardDedup bool
|
||
BeginDate string
|
||
EndDate string
|
||
}
|
||
type ChannelRepo interface {
|
||
List(context.Context, UserScope, ChannelFilter, ...ListOption) (*ChannelPage, error)
|
||
Bindings(context.Context, UserScope, string) ([]*Binding, error)
|
||
Find(context.Context, UserScope, ChannelKey) (*Channel, error)
|
||
StatChannels(context.Context, []*Channel) ([]*Channel, error)
|
||
Metrics(context.Context, []ChannelKey, []ChannelKey, bool) (*ChannelMetricData, error)
|
||
Games(context.Context, ChannelGameScope) ([]*ChannelGame, error)
|
||
}
|
||
type ChannelCreate struct {
|
||
SourceDB, Account, Password string
|
||
BusierID uint64
|
||
RegisterType int
|
||
}
|
||
type ChannelCreateResult struct {
|
||
ID uint64
|
||
SourceDB string
|
||
}
|
||
type ChannelWrite struct {
|
||
ChannelCreate
|
||
Status, Level, SettlementType, SettlementDays int
|
||
ParentID, TopPromoteID uint64
|
||
GameIDs2 string
|
||
CreatedUnix, NextCountUnix int64
|
||
}
|
||
type ChannelWriteRepo interface {
|
||
Create(context.Context, *ChannelWrite) (uint64, error)
|
||
}
|
||
type PlatformPasswordEncoder interface{ Encode(string) (string, error) }
|
||
type ChannelUsecase struct {
|
||
repo ChannelRepo
|
||
writer ChannelWriteRepo
|
||
accounts *AccountUsecase
|
||
preferences *UserPreferenceUsecase
|
||
passwords PlatformPasswordEncoder
|
||
effectiveness *EffectivenessUsecase
|
||
now func() time.Time
|
||
}
|
||
|
||
func NewChannelUsecase(repo ChannelRepo, writer ChannelWriteRepo, accounts *AccountUsecase, preferences *UserPreferenceUsecase, passwords PlatformPasswordEncoder, effectiveness *EffectivenessUsecase) *ChannelUsecase {
|
||
return &ChannelUsecase{repo: repo, writer: writer, accounts: accounts, preferences: preferences, passwords: passwords, now: time.Now, effectiveness: effectiveness}
|
||
}
|
||
func (uc *ChannelUsecase) scope(ctx context.Context) (UserScope, error) {
|
||
actor, a, err := uc.accounts.actor(ctx)
|
||
if err != nil {
|
||
return UserScope{}, err
|
||
}
|
||
if actor.AuthorityID == 888 {
|
||
return UserScope{All: true}, nil
|
||
}
|
||
return UserScope{SelfID: a.ID, IncludeSelf: true, OnlySelf: a.Level >= 3, DescendantPath: descendantPath(a)}, nil
|
||
}
|
||
func normalizeIDCard(raw string) string {
|
||
// Java isBlank 排除不换行空格;其余空白证件仍不计入去重人数。
|
||
if strings.TrimFunc(raw, func(r rune) bool { return unicode.IsSpace(r) && r != '\u00a0' && r != '\u2007' && r != '\u202f' }) == "" {
|
||
return ""
|
||
}
|
||
value := strings.TrimFunc(raw, func(r rune) bool { return r <= 32 })
|
||
if strings.HasSuffix(value, "X") {
|
||
value = strings.TrimSuffix(value, "X") + "x"
|
||
}
|
||
return value
|
||
}
|
||
func (uc *ChannelUsecase) List(ctx context.Context, filter ChannelFilter, options ...ListOption) (*ChannelPage, error) {
|
||
scope, err := uc.scope(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if uc.effectiveness != nil {
|
||
if err := uc.effectiveness.Ensure(ctx); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
page, err := uc.repo.List(ctx, scope, filter, options...)
|
||
if err != nil || len(page.Rows) == 0 {
|
||
return page, err
|
||
}
|
||
pref, err := uc.preferences.Current(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
statChannels := page.Rows
|
||
if pref.IncludeSubPromote {
|
||
statChannels, err = uc.repo.StatChannels(ctx, page.Rows)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
roots, statKeys := []ChannelKey{}, []ChannelKey{}
|
||
groups := map[ChannelKey][]ChannelKey{}
|
||
seen := map[ChannelKey]bool{}
|
||
for _, root := range page.Rows {
|
||
roots = append(roots, root.ChannelKey)
|
||
groupSeen := map[ChannelKey]bool{root.ChannelKey: true}
|
||
groups[root.ChannelKey] = []ChannelKey{root.ChannelKey}
|
||
for _, candidate := range statChannels {
|
||
if root.IncludesStat(candidate, pref.IncludeSubPromote) && !groupSeen[candidate.ChannelKey] {
|
||
groups[root.ChannelKey] = append(groups[root.ChannelKey], candidate.ChannelKey)
|
||
groupSeen[candidate.ChannelKey] = true
|
||
}
|
||
}
|
||
for _, key := range groups[root.ChannelKey] {
|
||
if !seen[key] {
|
||
statKeys = append(statKeys, key)
|
||
seen[key] = true
|
||
}
|
||
}
|
||
}
|
||
metrics, err := uc.repo.Metrics(ctx, roots, statKeys, pref.IDCardDedup)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
cards := map[ChannelKey][]ChannelPlayer{}
|
||
for _, player := range metrics.PlayerCards {
|
||
cards[player.ChannelKey] = append(cards[player.ChannelKey], player)
|
||
}
|
||
for _, root := range page.Rows {
|
||
if metrics.GamesAvailable {
|
||
count := metrics.Games[root.ChannelKey]
|
||
root.GameCount = &count
|
||
}
|
||
count := int64(0)
|
||
userCards := map[uint64]string{}
|
||
pay := new(big.Rat)
|
||
for _, key := range groups[root.ChannelKey] {
|
||
count += metrics.Players[key]
|
||
for _, player := range cards[key] {
|
||
userCards[player.UserID] = normalizeIDCard(player.IDCard)
|
||
}
|
||
if amount := metrics.Pay[key]; amount != "" {
|
||
n, ok := new(big.Rat).SetString(amount)
|
||
if !ok {
|
||
return nil, ErrStorage
|
||
}
|
||
pay.Add(pay, n)
|
||
}
|
||
}
|
||
if metrics.PlayersAvailable {
|
||
if pref.IDCardDedup {
|
||
identities := map[string]bool{}
|
||
for _, card := range userCards {
|
||
if card != "" {
|
||
identities[card] = true
|
||
}
|
||
}
|
||
count = int64(len(identities))
|
||
}
|
||
root.PlayerCount = &count
|
||
}
|
||
if metrics.PayAvailable {
|
||
amount := pay.FloatString(2)
|
||
root.TotalPayAmount = &amount
|
||
}
|
||
}
|
||
return page, nil
|
||
}
|
||
func (uc *ChannelUsecase) BindOptions(ctx context.Context, source string) ([]*Binding, error) {
|
||
source, err := NormalizePlatform(source)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
scope, err := uc.scope(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return uc.repo.Bindings(ctx, scope, source)
|
||
}
|
||
|
||
var channelAccountPattern = regexp.MustCompile(`^[\x{4e00}-\x{9fa5}a-zA-Z0-9]{4,30}$`)
|
||
|
||
func (uc *ChannelUsecase) Create(ctx context.Context, input *ChannelCreate) (*ChannelCreateResult, error) {
|
||
if input == nil {
|
||
return nil, ErrChannelInput
|
||
}
|
||
account := strings.TrimSpace(input.Account)
|
||
if !channelAccountPattern.MatchString(account) {
|
||
return nil, ErrChannelAccount
|
||
}
|
||
size := len(utf16.Encode([]rune(input.Password)))
|
||
if strings.TrimSpace(input.Password) == "" || size < 6 || size > 15 {
|
||
return nil, ErrChannelPassword
|
||
}
|
||
bindings, err := uc.BindOptions(ctx, input.SourceDB)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
valid := false
|
||
for _, binding := range bindings {
|
||
if binding.BusierID == input.BusierID {
|
||
valid = true
|
||
break
|
||
}
|
||
}
|
||
if !valid {
|
||
return nil, ErrChannelBinding
|
||
}
|
||
source, _ := NormalizePlatform(input.SourceDB)
|
||
password, err := uc.passwords.Encode(input.Password)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
now := uc.now().Unix()
|
||
write := &ChannelWrite{ChannelCreate: ChannelCreate{SourceDB: source, Account: account, Password: password, BusierID: input.BusierID, RegisterType: input.RegisterType}, Status: -3, Level: 1, SettlementType: 0, SettlementDays: 7, CreatedUnix: now, NextCountUnix: now + 7*24*60*60}
|
||
id, err := uc.writer.Create(ctx, write)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &ChannelCreateResult{ID: id, SourceDB: source}, nil
|
||
}
|
||
func (uc *ChannelUsecase) Games(ctx context.Context, key ChannelKey) ([]*ChannelGame, error) {
|
||
source, err := NormalizePlatform(key.SourceDB)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
key.SourceDB = source
|
||
scope, err := uc.scope(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
root, err := uc.repo.Find(ctx, scope, key)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
pref, err := uc.preferences.Current(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
day := uc.now().In(time.FixedZone("Asia/Shanghai", 8*3600)).Format("2006-01-02")
|
||
games, err := uc.repo.Games(ctx, ChannelGameScope{Root: root, IncludeSub: pref.IncludeSubPromote, IDCardDedup: pref.IDCardDedup, EndDate: day})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return normalizeChannelGames(games)
|
||
}
|
||
|
||
func normalizeChannelGames(games []*ChannelGame) ([]*ChannelGame, error) {
|
||
amounts := map[uint64]*big.Rat{}
|
||
for _, game := range games {
|
||
game.RegisterCount = min(game.RegisterCount, int64(2147483647))
|
||
n, ok := new(big.Rat).SetString(game.PayAmount)
|
||
if !ok {
|
||
return nil, ErrStorage
|
||
}
|
||
game.PayAmount = n.FloatString(2)
|
||
// 原画像先把金额保留两位再排序;同额时按游戏 ID 稳定排序。
|
||
amounts[game.ID], _ = new(big.Rat).SetString(game.PayAmount)
|
||
}
|
||
sort.Slice(games, func(i, j int) bool {
|
||
order := amounts[games[i].ID].Cmp(amounts[games[j].ID])
|
||
if order != 0 {
|
||
return order > 0
|
||
}
|
||
return games[i].ID < games[j].ID
|
||
})
|
||
return games, nil
|
||
}
|