204 lines
7.9 KiB
Go
204 lines
7.9 KiB
Go
package business
|
|
|
|
import (
|
|
"context"
|
|
"gorm.io/gorm"
|
|
"kra/internal/biz/business"
|
|
"math/big"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func alertTree(db *gorm.DB, roots []business.ChannelKey) *gorm.DB {
|
|
base := func() *gorm.DB {
|
|
return db.Table("tab_promote r").Where("(r.source_db,r.id) IN ?", channelPairs(roots))
|
|
}
|
|
self := base().Select("r.source_db,r.id,r.id AS leaf_id")
|
|
children := base().Joins("JOIN tab_promote child ON child.source_db=r.source_db AND child.parent_id=r.id").Select("r.source_db,r.id,child.id AS leaf_id")
|
|
descendants := base().Joins("JOIN tab_promote child ON child.source_db=r.source_db AND child.top_promote_id=r.id").Select("r.source_db,r.id,child.id AS leaf_id")
|
|
// Each branch can use its own equality index. UNION removes channels reached
|
|
// through both parent and top-level relationships without an OR join scan.
|
|
return db.Table("(? UNION ? UNION ?) root", self, children, descendants).
|
|
Joins("JOIN tab_promote p ON p.source_db=root.source_db AND p.id=root.leaf_id")
|
|
}
|
|
func alertMetricSpec(db *gorm.DB, metric int) (table, stamp, condition, amount string) {
|
|
if metric == 2 {
|
|
return "tab_spend", "m.pay_time", "m.pay_status=1", "m.pay_amount"
|
|
}
|
|
stamp = "CAST(m.register_time AS INTEGER)"
|
|
if db.Dialector.Name() == "mysql" {
|
|
stamp = "m.sw_register_time_unix"
|
|
}
|
|
return "tab_user", stamp, "(m.puid=0 OR m.puid IS NULL)", "1"
|
|
}
|
|
|
|
func (r *alertTriggerRepo) Comparison(ctx context.Context, metric int, channels []business.ChannelKey, today time.Time, days int) (map[business.ChannelKey]business.AlertComparison, error) {
|
|
db := r.data.DB().WithContext(ctx)
|
|
if metric != 1 && metric != 2 {
|
|
return nil, business.ErrAlertRuleInput
|
|
}
|
|
table, stamp, condition, amount := alertMetricSpec(db, metric)
|
|
available, err := businessTablesAvailable(db, table, "tab_promote")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !available {
|
|
return nil, business.ErrReportBuildUnavailable
|
|
}
|
|
result := map[business.ChannelKey]business.AlertComparison{}
|
|
current, previous := today.AddDate(0, 0, -days).Unix(), today.AddDate(0, 0, -2*days).Unix()
|
|
// 先取小规模渠道映射,再按固定平台及渠道 ID 批量聚合;避免 MySQL 5.7
|
|
// 将动态 JOIN 的时间范围退化为 ref 扫描,读取每个渠道的全部历史明细。
|
|
owners := map[business.ChannelKey][]business.ChannelKey{}
|
|
bySource := map[string][]uint64{}
|
|
for start := 0; start < len(channels); start += 100 {
|
|
var pairs []struct {
|
|
SourceDB string
|
|
RootID, PromoteID uint64
|
|
}
|
|
if err := alertTree(db, channels[start:min(start+100, len(channels))]).Select("root.source_db,root.id AS root_id,p.id AS promote_id").Scan(&pairs).Error; err != nil {
|
|
return nil, business.ErrStorage
|
|
}
|
|
for _, p := range pairs {
|
|
leaf := business.ChannelKey{SourceDB: p.SourceDB, ID: p.PromoteID}
|
|
root := business.ChannelKey{SourceDB: p.SourceDB, ID: p.RootID}
|
|
if len(owners[leaf]) == 0 {
|
|
bySource[p.SourceDB] = append(bySource[p.SourceDB], p.PromoteID)
|
|
}
|
|
owners[leaf] = append(owners[leaf], root)
|
|
result[root] = business.AlertComparison{Current: "0", Previous: "0"}
|
|
}
|
|
}
|
|
for source, ids := range bySource {
|
|
for start := 0; start < len(ids); start += 300 {
|
|
batch := ids[start:min(start+300, len(ids))]
|
|
firstQueries := []string{}
|
|
args := []any{}
|
|
for _, id := range batch {
|
|
// MIN 的三个等值前缀命中覆盖索引,MySQL 可直接读取首个索引项。
|
|
firstQueries = append(firstQueries, "SELECT ? AS promote_id,(SELECT MIN("+stamp+") FROM "+table+" m WHERE m.source_db=? AND m.promote_id=? AND "+condition+" AND "+stamp+">0) AS first_unix")
|
|
args = append(args, id, source, id)
|
|
}
|
|
var first []struct {
|
|
PromoteID uint64
|
|
FirstUnix *int64
|
|
}
|
|
if err := db.Raw(strings.Join(firstQueries, " UNION ALL "), args...).Scan(&first).Error; err != nil {
|
|
return nil, business.ErrStorage
|
|
}
|
|
for _, row := range first {
|
|
if row.FirstUnix == nil {
|
|
continue
|
|
}
|
|
for _, root := range owners[business.ChannelKey{SourceDB: source, ID: row.PromoteID}] {
|
|
v := result[root]
|
|
if v.FirstUnix == 0 || *row.FirstUnix < v.FirstUnix {
|
|
v.FirstUnix = *row.FirstUnix
|
|
}
|
|
result[root] = v
|
|
}
|
|
}
|
|
var rows []struct {
|
|
PromoteID uint64
|
|
CurrentValue, PreviousValue string
|
|
}
|
|
selection := "m.promote_id,COALESCE(SUM(CASE WHEN " + stamp + ">=? THEN " + amount + " ELSE 0 END),0) AS current_value,COALESCE(SUM(CASE WHEN " + stamp + "<? THEN " + amount + " ELSE 0 END),0) AS previous_value"
|
|
q := db.Table(table+" m").Where("m.source_db=? AND m.promote_id IN ?", source, batch).Where(condition).Where(stamp+">=? AND "+stamp+"<?", previous, today.Unix())
|
|
if err := q.Select(selection, current, current).Group("m.promote_id").Scan(&rows).Error; err != nil {
|
|
return nil, business.ErrStorage
|
|
}
|
|
scale := 0
|
|
if metric == 2 {
|
|
scale = 2
|
|
}
|
|
for _, row := range rows {
|
|
for _, root := range owners[business.ChannelKey{SourceDB: source, ID: row.PromoteID}] {
|
|
v := result[root]
|
|
var err error
|
|
v.Current, err = addAlertMetric(v.Current, row.CurrentValue, scale)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
v.Previous, err = addAlertMetric(v.Previous, row.PreviousValue, scale)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result[root] = v
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func addAlertMetric(a, b string, scale int) (string, error) {
|
|
left, ok := new(big.Rat).SetString(a)
|
|
if !ok {
|
|
return "", business.ErrStorage
|
|
}
|
|
right, ok := new(big.Rat).SetString(b)
|
|
if !ok {
|
|
return "", business.ErrStorage
|
|
}
|
|
return left.Add(left, right).FloatString(scale), nil
|
|
}
|
|
|
|
func (r *alertTriggerRepo) Snapshots(ctx context.Context, channels []business.ChannelKey, now time.Time, dedup bool) (map[business.ChannelKey]business.AlertSnapshot, error) {
|
|
db := r.data.DB().WithContext(ctx)
|
|
available, err := businessTablesAvailable(db, "tab_user", "tab_spend", "tab_promote")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !available {
|
|
return nil, business.ErrReportBuildUnavailable
|
|
}
|
|
result := map[business.ChannelKey]business.AlertSnapshot{}
|
|
for _, key := range channels {
|
|
result[key] = business.AlertSnapshot{Pay: "0.00"}
|
|
}
|
|
for start := 0; start < len(channels); start += 100 {
|
|
batch := channels[start:min(start+100, len(channels))]
|
|
var players []struct {
|
|
SourceDB string
|
|
PromoteID uint64
|
|
Players int64
|
|
}
|
|
_, stamp, condition, _ := alertMetricSpec(db, 1)
|
|
identity := "CAST(m.id AS TEXT)"
|
|
if db.Dialector.Name() == "mysql" {
|
|
identity = "CAST(m.id AS CHAR)"
|
|
}
|
|
if dedup {
|
|
identity = "CASE WHEN TRIM(COALESCE(m.idcard,''))<>'' THEN 'card:' || TRIM(m.idcard) ELSE 'user:' || " + identity + " END"
|
|
if db.Dialector.Name() == "mysql" {
|
|
identity = "CASE WHEN TRIM(COALESCE(m.idcard,''))<>'' THEN CONCAT('card:',TRIM(m.idcard)) ELSE CONCAT('user:',m.id) END"
|
|
}
|
|
}
|
|
q := alertTree(db, batch).Joins("JOIN tab_user m ON m.source_db=p.source_db AND m.promote_id=p.id").Where(condition).Where(stamp+"<=?", now.Unix())
|
|
if err := q.Select("root.source_db,root.id AS promote_id,COUNT(DISTINCT " + identity + ") AS players").Group("root.source_db,root.id").Scan(&players).Error; err != nil {
|
|
return nil, business.ErrStorage
|
|
}
|
|
for _, row := range players {
|
|
key := business.ChannelKey{SourceDB: row.SourceDB, ID: row.PromoteID}
|
|
v := result[key]
|
|
v.Players = row.Players
|
|
result[key] = v
|
|
}
|
|
var payments []struct {
|
|
SourceDB string
|
|
PromoteID uint64
|
|
Amount string
|
|
}
|
|
if err := alertTree(db, batch).Joins("JOIN tab_spend m ON m.source_db=p.source_db AND m.promote_id=p.id").Where("m.pay_status=1 AND m.pay_time<=?", now.Unix()).Select("root.source_db,root.id AS promote_id,COALESCE(SUM(m.pay_amount),0) AS amount").Group("root.source_db,root.id").Scan(&payments).Error; err != nil {
|
|
return nil, business.ErrStorage
|
|
}
|
|
for _, row := range payments {
|
|
key := business.ChannelKey{SourceDB: row.SourceDB, ID: row.PromoteID}
|
|
v := result[key]
|
|
v.Pay = row.Amount
|
|
result[key] = v
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|