Files
kra-new/internal/biz/business/channel.go
T

375 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}