Files
kra-new/internal/data/business/performance.go
T

233 lines
11 KiB
Go

package business
import (
"context"
"time"
"gorm.io/gorm"
"kra/internal/biz/business"
)
type performanceRepo struct{ data Provider }
func NewPerformanceRepo(data Provider) business.PerformanceRepo { return &performanceRepo{data: data} }
func performanceChannels(db *gorm.DB, scope business.UserScope, owners []uint, sources []string) *gorm.DB {
q := db.Table("tab_promote p").Where("p.source_db IN ?", []string{"lx", "hw", "yq"})
if len(sources) > 0 {
q = q.Where("p.source_db IN ?", sources)
}
if !scope.All || owners != nil {
bindings := channelBindingQuery(db, scope, business.ChannelFilter{Sources: sources}).Select("b.source_db,b.platform_busier_id")
if owners != nil {
bindings = bindings.Where("b.user_id IN ?", owners)
}
q = q.Where("(p.source_db,p.busier_id) IN (?)", bindings)
}
return q
}
func performancePayQuery(db *gorm.DB, scope business.UserScope, owners []uint, sources []string, begin, end time.Time) *gorm.DB {
keys := performanceChannels(db, scope, owners, sources).Select("p.source_db,p.id")
return db.Table("tab_sw_report_day r").Where("r.dedup_mode=0 AND r.game_id>0 AND r.stat_date BETWEEN ? AND ?", begin.Format("2006-01-02"), end.Format("2006-01-02")).Where("(r.source_db,r.promote_id) IN (?)", keys)
}
func joinEffectiveness(q *gorm.DB) *gorm.DB {
return q.Joins("LEFT JOIN sys_business_effectiveness_state es ON es.id=1").Joins("LEFT JOIN sys_business_channel_effectiveness e ON e.rule_key=es.rule_key AND e.source_db=p.source_db AND e.promote_id=p.id")
}
type performanceChannelPO struct {
ContributorID uint
ContributorName string
ID, BusierID uint64
SourceDB string `gorm:"column:source_db"`
Account, Nickname string
CreateTime, QualifiedAt *int64
WindowPay, PeriodPay string
}
func performanceChannelToBiz(po performanceChannelPO) *business.PerformanceChannel {
return &business.PerformanceChannel{ContributorID: po.ContributorID, ContributorName: po.ContributorName, ChannelKey: business.ChannelKey{ID: po.ID, SourceDB: po.SourceDB}, Account: po.Account, Name: po.Nickname, CreatedAt: po.CreateTime, QualifiedAt: po.QualifiedAt, WindowPay: po.WindowPay, PeriodPay: po.PeriodPay, OwnerIDs: []uint{}}
}
type performanceOwnerPO struct {
SourceDB string `gorm:"column:source_db"`
PlatformBusierID uint64
UserID uint
Username, NickName string
}
func performanceOwners(db *gorm.DB, scope business.UserScope, sources []string) (map[business.PlatformKey][]performanceOwnerPO, error) {
var rows []performanceOwnerPO
if err := channelBindingQuery(db, scope, business.ChannelFilter{Sources: sources}).Select("b.source_db,b.platform_busier_id,b.user_id,u.username,u.nick_name").Order("b.user_id").Scan(&rows).Error; err != nil {
return nil, business.ErrStorage
}
byKey := map[business.PlatformKey][]performanceOwnerPO{}
for _, row := range rows {
key := business.PlatformKey{SourceDB: row.SourceDB, BusierID: row.PlatformBusierID}
byKey[key] = append(byKey[key], row)
}
return byKey, nil
}
func (r *performanceRepo) Facts(ctx context.Context, scope business.UserScope, ownerIDs []uint, sources []string, begin, end time.Time) (*business.PerformanceFacts, error) {
var out *business.PerformanceFacts
err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var err error
out, err = readPerformanceFacts(tx, scope, ownerIDs, sources, begin, end)
return err
})
return out, storageError(err)
}
func readPerformanceFacts(db *gorm.DB, scope business.UserScope, ownerIDs []uint, sources []string, begin, end time.Time) (*business.PerformanceFacts, error) {
out := &business.PerformanceFacts{Channels: []*business.PerformanceChannel{}, Cash: []business.PerformanceCash{}, Available: true}
available, err := businessTablesAvailable(db, "tab_promote", "tab_sw_report_day")
if err != nil {
return nil, err
}
out.Available = available
if !available {
return out, nil
}
owners, err := performanceOwners(db, scope, sources)
if err != nil {
return nil, err
}
var pos []performanceChannelPO
start, finish := begin.Unix(), min(end.AddDate(0, 0, 1).Unix(), time.Now().Unix()+1)
channelFacts := joinEffectiveness(performanceChannels(db, scope, ownerIDs, sources)).Where("(p.create_time>=? AND p.create_time<?) OR (e.qualified_at>=? AND e.qualified_at<?)", start, finish, start, finish)
if err := channelFacts.Select("p.id,p.source_db,p.busier_id,p.account,p.nickname,p.create_time,e.qualified_at,COALESCE(e.window_pay,0) AS window_pay").Scan(&pos).Error; err != nil {
return nil, business.ErrStorage
}
for _, po := range pos {
channel := performanceChannelToBiz(po)
for _, owner := range owners[business.PlatformKey{SourceDB: po.SourceDB, BusierID: po.BusierID}] {
channel.OwnerIDs = append(channel.OwnerIDs, owner.UserID)
if channel.OwnerID == 0 {
channel.OwnerID = owner.UserID
channel.OwnerName = owner.NickName
if channel.OwnerName == "" {
channel.OwnerName = owner.Username
}
}
}
out.Channels = append(out.Channels, channel)
}
if begin.After(end) {
return out, nil
}
var cash []struct {
SourceDB string `gorm:"column:source_db"`
BusierID uint64
StatDate, Pay string
}
// Aggregate once per SDK business/date, not per channel/date. All channels
// under the same SDK owner have the same business-tree attribution. This
// keeps the exact totals while avoiding hundreds of thousands of Go rows.
visibleChannels := performanceChannels(db, scope, ownerIDs, sources).Select("p.source_db,p.id,p.busier_id")
if err := db.Table("tab_sw_report_day r").Joins("JOIN (?) owner_p ON owner_p.source_db=r.source_db AND owner_p.id=r.promote_id", visibleChannels).Where("r.dedup_mode=0 AND r.game_id>0 AND r.stat_date BETWEEN ? AND ?", begin.Format("2006-01-02"), end.Format("2006-01-02")).Select("r.source_db,owner_p.busier_id,CAST(r.stat_date AS CHAR) AS stat_date,CAST(SUM(r.total_pay) AS CHAR) AS pay").Group("r.source_db,owner_p.busier_id,r.stat_date").Scan(&cash).Error; err != nil {
return nil, business.ErrStorage
}
for _, row := range cash {
ids := []uint{}
for _, owner := range owners[business.PlatformKey{SourceDB: row.SourceDB, BusierID: row.BusierID}] {
ids = append(ids, owner.UserID)
}
out.Cash = append(out.Cash, business.PerformanceCash{ChannelKey: business.ChannelKey{SourceDB: row.SourceDB, ID: row.BusierID}, OwnerIDs: ids, Date: row.StatDate, Pay: row.Pay})
}
return out, nil
}
func (r *performanceRepo) Details(ctx context.Context, scope business.UserScope, selection business.PerformanceDetailSelection, sources []string, begin, end time.Time, metric string, offset, limit int) ([]*business.PerformanceChannel, int64, error) {
db := r.data.DB().WithContext(ctx)
owners := selection.OwnerIDs
pay := performancePayQuery(db, scope, owners, sources, begin, end).Select("r.source_db,r.promote_id,SUM(r.total_pay) AS pay").Group("r.source_db,r.promote_id")
needsPay := metric == "pay" || metric == "all"
q := joinEffectiveness(performanceChannels(db, scope, owners, sources))
contributorSelect, contributorOrder := "", ""
if len(selection.ContributorIDs) > 0 {
// Keep one row per member/platform owner, even if bindings overlap.
prefix := "(COALESCE(NULLIF(TRIM(c.id_path),''),'0') || ',' || c.id)"
pattern := "(" + prefix + " || ',%')"
if db.Dialector.Name() == "mysql" {
prefix = "CONCAT(COALESCE(NULLIF(TRIM(c.id_path),''),'0'),',',c.id)"
pattern = "CONCAT(" + prefix + ",',%')"
}
contributors := channelBindingQuery(db, scope, business.ChannelFilter{Sources: sources}).
Joins("JOIN sys_users c ON c.deleted_at IS NULL AND (c.id=u.id OR c.id=u.parent_id OR u.id_path="+prefix+" OR u.id_path LIKE "+pattern+")").
Where("c.id IN ? AND u.deleted_at IS NULL", selection.ContributorIDs).
Select("DISTINCT b.source_db,b.platform_busier_id,c.id AS contributor_id,COALESCE(NULLIF(c.nick_name,''),c.username) AS contributor_name")
if owners != nil {
contributors = contributors.Where("b.user_id IN ?", owners)
}
q = q.Joins("JOIN (?) contribution ON contribution.source_db=p.source_db AND contribution.platform_busier_id=p.busier_id", contributors)
contributorSelect = ",contribution.contributor_id,contribution.contributor_name"
contributorOrder = ",contribution.contributor_id"
}
if needsPay {
q = q.Joins("LEFT JOIN (?) pp ON pp.source_db=p.source_db AND pp.promote_id=p.id", pay)
}
start, finish := begin.Unix(), min(end.AddDate(0, 0, 1).Unix(), time.Now().Unix()+1)
switch metric {
case "open":
q = q.Where("p.create_time>=? AND p.create_time<?", start, finish)
case "effective":
q = q.Where("e.qualified_at>=? AND e.qualified_at<?", start, finish)
case "pay":
q = q.Where("COALESCE(pp.pay,0)<>0")
default:
q = q.Where("(p.create_time>=? AND p.create_time<?) OR (e.qualified_at>=? AND e.qualified_at<?) OR COALESCE(pp.pay,0)<>0", start, finish, start, finish)
}
var total int64
if err := q.Count(&total).Error; err != nil {
return nil, 0, business.ErrStorage
}
var pos []performanceChannelPO
paySelect := "'0.00' AS period_pay"
if needsPay {
paySelect = "CAST(COALESCE(pp.pay,0) AS CHAR) AS period_pay"
}
if err := q.Select("p.id,p.source_db,p.busier_id,p.account,p.nickname,p.create_time,e.qualified_at,CAST(COALESCE(e.window_pay,0) AS CHAR) AS window_pay," + paySelect + contributorSelect).Order("p.create_time DESC,p.source_db,p.id" + contributorOrder).Offset(offset).Limit(limit).Scan(&pos).Error; err != nil {
return nil, 0, business.ErrStorage
}
if !needsPay && len(pos) > 0 {
keys := [][]any{}
for _, po := range pos {
keys = append(keys, []any{po.SourceDB, po.ID})
}
var amounts []struct {
SourceDB string `gorm:"column:source_db"`
PromoteID uint64
Pay string
}
if err := db.Table("tab_sw_report_day r").Where("r.dedup_mode=0 AND r.game_id>0 AND r.stat_date BETWEEN ? AND ? AND (r.source_db,r.promote_id) IN ?", begin.Format("2006-01-02"), end.Format("2006-01-02"), keys).Select("r.source_db,r.promote_id,CAST(SUM(r.total_pay) AS CHAR) AS pay").Group("r.source_db,r.promote_id").Scan(&amounts).Error; err != nil {
return nil, 0, business.ErrStorage
}
byKey := map[business.ChannelKey]string{}
for _, a := range amounts {
byKey[business.ChannelKey{SourceDB: a.SourceDB, ID: a.PromoteID}] = a.Pay
}
for i := range pos {
if value, ok := byKey[business.ChannelKey{SourceDB: pos[i].SourceDB, ID: pos[i].ID}]; ok {
pos[i].PeriodPay = value
}
}
}
bindings, err := performanceOwners(db, scope, sources)
if err != nil {
return nil, 0, err
}
rows := make([]*business.PerformanceChannel, 0, len(pos))
for _, po := range pos {
row := performanceChannelToBiz(po)
for _, owner := range bindings[business.PlatformKey{SourceDB: po.SourceDB, BusierID: po.BusierID}] {
row.OwnerIDs = append(row.OwnerIDs, owner.UserID)
if row.OwnerID == 0 {
row.OwnerID = owner.UserID
row.OwnerName = owner.NickName
if row.OwnerName == "" {
row.OwnerName = owner.Username
}
}
}
rows = append(rows, row)
}
return rows, total, nil
}