滞回标志按 app+pool 单行存储且由每个用户钉住的组内配置推进; 两组三元组不同(尤其 control 关/treatment 开的灰度形态)会互踩: control 逐抽清掉 treatment 的标志,阈值不同的组按对方状态加权付奖。 加权特性的灰度必须走常规发布,不能走实验。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
163 lines
7.3 KiB
Go
163 lines
7.3 KiB
Go
package databi
|
||
|
||
// 幸运礼物V3 section:数据来自 lucky-gift-service 的按日读模型(lucky_draw_pool_day_stats),
|
||
// 不走 statistics-service。注入方式与 Finance overlay 同层:appRequirements 组装 sections 时追加。
|
||
// 口径:只统计 dynamic_v3 策略族的房间抽奖(不含外部接入),返奖为用户可见金额(含未发放/失败行)。
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
"time"
|
||
|
||
luckygiftv1 "hyapp.local/api/proto/luckygift/v1"
|
||
)
|
||
|
||
const luckyGiftV3SectionKey = "lucky_gift_v3"
|
||
|
||
// luckyGiftV3MaxDays 与 lucky-gift-service 侧的窗口上限对齐,防御异常长区间把零填充行撑爆。
|
||
const luckyGiftV3MaxDays = 400
|
||
|
||
// luckyGiftStatZone 与 lucky_draw_pool_day_stats 的北京日分桶一致;FixedZone 规避容器缺 tzdata。
|
||
var luckyGiftStatZone = time.FixedZone("Asia/Shanghai", 8*60*60)
|
||
|
||
// LuckyGiftDailyStatsSource 是 databi 对幸运礼物统计的最小依赖面;luckygiftadmin.Client 天然满足。
|
||
type LuckyGiftDailyStatsSource interface {
|
||
ListLuckyGiftDailyStats(ctx context.Context, req *luckygiftv1.ListLuckyGiftDailyStatsRequest) (*luckygiftv1.ListLuckyGiftDailyStatsResponse, error)
|
||
}
|
||
|
||
// WithLuckyGiftDailyStats 注入幸运礼物按日统计源;不注入时数据需求宽表没有幸运礼物V3板块。
|
||
func WithLuckyGiftDailyStats(source LuckyGiftDailyStatsSource, requestTimeout time.Duration) ServiceOption {
|
||
return func(s *Service) {
|
||
s.luckyGiftStats = source
|
||
s.luckyGiftTimeout = requestTimeout
|
||
}
|
||
}
|
||
|
||
// requirementsIncludesLuckyGiftV3 与 requirementsIncludesRevenue 同语义:空/all 视为命中。
|
||
func requirementsIncludesLuckyGiftV3(section string) bool {
|
||
switch strings.ToLower(strings.TrimSpace(section)) {
|
||
case "", "all", luckyGiftV3SectionKey:
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// requirementsOnlyLuckyGiftV3 为真时整个请求只要幸运礼物板块;此时必须跳过 statistics-service——
|
||
// 它不认识该 section key,会回退成全量六板块的最重查询路径。
|
||
func requirementsOnlyLuckyGiftV3(section string) bool {
|
||
return strings.ToLower(strings.TrimSpace(section)) == luckyGiftV3SectionKey
|
||
}
|
||
|
||
// luckyGiftV3RegionIDs 与 Finance overlay 相同的区域参数收敛:单区域查询归一化后覆盖多区域列表。
|
||
func luckyGiftV3RegionIDs(query RequirementsQuery, regions []RegionInfo, regionIDs []int64) []int64 {
|
||
if query.RegionID > 0 {
|
||
return []int64{normalizeRegionID(regions, query.RegionID)}
|
||
}
|
||
return regionIDs
|
||
}
|
||
|
||
// luckyGiftV3StatDays 按北京日展开 [startMS, endMS) 覆盖的日列表,用于零填充。
|
||
func luckyGiftV3StatDays(startMS, endMS int64) []string {
|
||
if endMS <= startMS {
|
||
return nil
|
||
}
|
||
start := time.UnixMilli(startMS).In(luckyGiftStatZone)
|
||
end := time.UnixMilli(endMS - 1).In(luckyGiftStatZone)
|
||
startDay := time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, luckyGiftStatZone)
|
||
endDay := time.Date(end.Year(), end.Month(), end.Day(), 0, 0, 0, 0, luckyGiftStatZone)
|
||
days := make([]string, 0, 32)
|
||
for day := startDay; !day.After(endDay) && len(days) < luckyGiftV3MaxDays; day = day.AddDate(0, 0, 1) {
|
||
days = append(days, day.Format("2006-01-02"))
|
||
}
|
||
return days
|
||
}
|
||
|
||
// luckyGiftV3Row 组装单行;比例在投入为 0 时输出 nil(前端渲染 "--"),不伪造 0%。
|
||
func luckyGiftV3Row(statDay, label string, drawCount, turnoverCoin, payoutCoin float64) map[string]any {
|
||
row := map[string]any{
|
||
"stat_day": statDay,
|
||
"label": label,
|
||
"draw_count": drawCount,
|
||
"turnover_coin": turnoverCoin,
|
||
"payout_coin": payoutCoin,
|
||
"profit_coin": turnoverCoin - payoutCoin,
|
||
}
|
||
if turnoverCoin > 0 {
|
||
row["payout_rate"] = payoutCoin / turnoverCoin
|
||
row["profit_rate"] = (turnoverCoin - payoutCoin) / turnoverCoin
|
||
} else {
|
||
row["payout_rate"] = nil
|
||
row["profit_rate"] = nil
|
||
}
|
||
return row
|
||
}
|
||
|
||
func luckyGiftV3Columns() []map[string]any {
|
||
return []map[string]any{
|
||
{"key": "draw_count", "label": "抽奖次数", "type": "count", "tooltip": "dynamic_v3 策略下的房间付费抽奖次数(不含外部 App 接入)"},
|
||
{"key": "turnover_coin", "label": "投入金币", "type": "coin", "tooltip": "抽奖消耗金币"},
|
||
{"key": "payout_coin", "label": "返奖金币", "type": "coin", "tooltip": "用户可见返奖金币,含未发放/失败行,与幸运礼物后台汇总口径一致"},
|
||
{"key": "profit_coin", "label": "利润金币", "type": "coin", "tooltip": "投入金币 - 返奖金币"},
|
||
{"key": "payout_rate", "label": "返奖率", "type": "ratio", "tooltip": "返奖金币 / 投入金币(实际 RTP)"},
|
||
{"key": "profit_rate", "label": "利润率", "type": "ratio", "tooltip": "利润金币 / 投入金币"},
|
||
}
|
||
}
|
||
|
||
// luckyGiftV3Section 拉取 dynamic_v3 按日统计并组装成数据需求 section。
|
||
// 数据按北京日零填充整个查询窗口:v3 未开量时明确展示 0,而不是空表让人误以为接口失败。
|
||
func (s *Service) luckyGiftV3Section(ctx context.Context, appCode string, query RequirementsQuery, regionIDs []int64) (map[string]any, error) {
|
||
if s.luckyGiftTimeout > 0 {
|
||
var cancel context.CancelFunc
|
||
ctx, cancel = context.WithTimeout(ctx, s.luckyGiftTimeout)
|
||
defer cancel()
|
||
}
|
||
resp, err := s.luckyGiftStats.ListLuckyGiftDailyStats(ctx, &luckygiftv1.ListLuckyGiftDailyStatsRequest{
|
||
Meta: &luckygiftv1.RequestMeta{Caller: "admin-server", AppCode: appCode, SentAtMs: time.Now().UnixMilli()},
|
||
StrategyVersion: "dynamic_v3",
|
||
StartMs: query.StartMS,
|
||
EndMs: query.EndMS,
|
||
RegionIds: regionIDs,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
byDay := map[string]*luckygiftv1.LuckyGiftDailyStat{}
|
||
for _, stat := range resp.GetStats() {
|
||
byDay[stat.GetStatDay()] = stat
|
||
}
|
||
days := luckyGiftV3StatDays(query.StartMS, query.EndMS)
|
||
rows := make([]map[string]any, 0, len(days))
|
||
var totalDraws, totalTurnover, totalPayout int64
|
||
for _, day := range days {
|
||
stat := byDay[day]
|
||
rows = append(rows, luckyGiftV3Row(day, day, float64(stat.GetDrawCount()), float64(stat.GetTurnoverCoin()), float64(stat.GetPayoutCoin())))
|
||
totalDraws += stat.GetDrawCount()
|
||
totalTurnover += stat.GetTurnoverCoin()
|
||
totalPayout += stat.GetPayoutCoin()
|
||
}
|
||
section := map[string]any{
|
||
"key": luckyGiftV3SectionKey,
|
||
"label": "幸运礼物V3",
|
||
// 幸运礼物没有用户身份/付费身份/新老用户维度,筛选恒为 all;前端该 tab 不渲染筛选器。
|
||
"filters": map[string]any{"user_role": "all", "payer_type": "all", "new_user_type": "all"},
|
||
"strategy_version": "dynamic_v3",
|
||
"columns": luckyGiftV3Columns(),
|
||
"metric_definitions": []map[string]any{
|
||
{"metric": "draw_count", "definition": "dynamic_v3 策略规则版本下的房间付费抽奖次数"},
|
||
{"metric": "payout_coin", "definition": "用户可见返奖(effective),含未发放/失败行;口径与幸运礼物后台汇总一致"},
|
||
{"metric": "payout_rate", "definition": "汇总/平均值行的比例按区间合计重新计算,不是逐日比例的算术平均"},
|
||
},
|
||
"total": luckyGiftV3Row("total", "汇总", float64(totalDraws), float64(totalTurnover), float64(totalPayout)),
|
||
"daily_series": rows,
|
||
}
|
||
if dayCount := len(days); dayCount > 0 {
|
||
average := luckyGiftV3Row("average", "平均值",
|
||
float64(totalDraws)/float64(dayCount),
|
||
float64(totalTurnover)/float64(dayCount),
|
||
float64(totalPayout)/float64(dayCount),
|
||
)
|
||
section["average"] = average
|
||
}
|
||
return section, nil
|
||
}
|