代码提交
This commit is contained in:
+20
-17
@@ -87,14 +87,15 @@ type TfInfo struct {
|
||||
|
||||
// 返回下载链接
|
||||
type PageButtonClickResp struct {
|
||||
VersionNum string `from:"versionNum" json:"versionNum"` //版本号
|
||||
Terminal string `from:"terminal" json:"terminal"` //终端ios android light
|
||||
Link []string `from:"link" json:"link"` //下载链接
|
||||
Tf []TfInfo `from:"tf" json:"tf"` //TFinfo
|
||||
BackUrl string `bson:"backUrl" json:"BackUrl"` //企业签URL (不用)
|
||||
PackageType versionMod.PkgType `from:"packageType" json:"packageType" binding:"required"` //包类型 1:企业包 2:商店包 3:TF包
|
||||
Status bool `from:"status" json:"status"` //是否启用
|
||||
LightData []byte `from:"lightData" json:"lightData"` //如果是轻量版,返回数据
|
||||
VersionNum string `from:"versionNum" json:"versionNum"` //版本号
|
||||
Terminal string `from:"terminal" json:"terminal"` //终端ios android light
|
||||
Link []string `from:"link" json:"link"` //下载链接
|
||||
Tf []TfInfo `from:"tf" json:"tf"` //TFinfo
|
||||
BackUrl string `bson:"backUrl" json:"BackUrl"` //企业签URL (不用)
|
||||
PackageType versionMod.PkgType `from:"packageType" json:"packageType" binding:"required"` //包类型 1:企业包 2:商店包 3:TF包
|
||||
Status bool `from:"status" json:"status"` //是否启用
|
||||
LightData []byte `from:"lightData" json:"lightData"` //如果是轻量版,返回数据
|
||||
ChannelApkAndDownUrl string `json:"channelApkAndDownUrl"` //双端下载配置里该渠道的Android下载地址,有则客户端优先用它
|
||||
}
|
||||
|
||||
// 落地页获取显示的按钮信息(appid标示具体应用)
|
||||
@@ -107,15 +108,17 @@ type PageButtonListReq struct {
|
||||
|
||||
// 落地页获取显示的按钮信息
|
||||
type PageButtonListResp struct {
|
||||
IosDown bool `json:"iosDown"` // 显示完整版,是企业包
|
||||
IosStore bool `json:"iosStore"` // 显示商店包
|
||||
IosTF bool `json:"iosTF"` // TF包
|
||||
IosH5 bool `json:"iosH5"` // H5跳转链接
|
||||
IosLink string `json:"iosLink"` // 商店包下载链接
|
||||
AndroidButton int `json:"androidButton"` // android下载按钮
|
||||
HasChannelApk bool `json:"hasChannelApk"` // 有安卓渠道包
|
||||
PictureList []PageButtonPicture `json:"pictureList"` // 资源数据
|
||||
Contact ContactInfo `json:"contact"` // 联系方式
|
||||
IosDown bool `json:"iosDown"` // 显示完整版,是企业包
|
||||
IosStore bool `json:"iosStore"` // 显示商店包
|
||||
IosTF bool `json:"iosTF"` // TF包
|
||||
IosH5 bool `json:"iosH5"` // H5跳转链接
|
||||
IosLink string `json:"iosLink"` // 商店包下载链接
|
||||
AndroidButton int `json:"androidButton"` // android下载按钮
|
||||
HasChannelApk bool `json:"hasChannelApk"` // 有安卓渠道包
|
||||
// 双端下载配置里该渠道的 Android 下载地址,有则落地页优先用它
|
||||
ChannelApkAndDownUrl string `json:"channelApkAndDownUrl"`
|
||||
PictureList []PageButtonPicture `json:"pictureList"` // 资源数据
|
||||
Contact ContactInfo `json:"contact"` // 联系方式
|
||||
}
|
||||
|
||||
type PageButtonPicture struct {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"stat/common/statutils"
|
||||
"time"
|
||||
)
|
||||
|
||||
func LineCount(iTemp string, iValue int64, permilValue int64) string {
|
||||
@@ -442,7 +443,7 @@ func (s *BotHourStat) GenerateTodayTable(title string, statName string, filepath
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BotHourStat) DisplayDayHourTextWithTitle(appName string, compare *BotHourStat) (text string, title string, descript string) {
|
||||
func (s *BotHourStat) DisplayDayHourTextWithTitle(appName string, compare, lastWeek *BotHourStat) (text string, title string, descript string) {
|
||||
calc := func(curr, hist *statutils.Stat) (*statutils.Stat, *statutils.Stat) {
|
||||
diff := curr.Sub(hist)
|
||||
rate := diff.Permil(hist)
|
||||
@@ -587,6 +588,7 @@ func (s *BotHourStat) DisplayDayHourTextWithTitle(appName string, compare *BotHo
|
||||
text += fmt.Sprintf("┗游戏下分: %d\n", s.TodayGame.DecAmount)
|
||||
}
|
||||
}()
|
||||
text += s.DisplayCompareText("9️⃣", compare, lastWeek)
|
||||
text += fmt.Sprintf("🆚%s同比数据:当日的昨天%d点\n", appName, compare.SumDate.Hour())
|
||||
} else {
|
||||
func() {
|
||||
@@ -693,7 +695,103 @@ func (s *BotHourStat) DisplayDayHourTextWithTitle(appName string, compare *BotHo
|
||||
text += fmt.Sprintf(" ┗游戏下分: %d\n", s.TodayGame.DecAmount)
|
||||
}
|
||||
}()
|
||||
text += s.DisplayCompareText("6️⃣", compare, lastWeek)
|
||||
text += fmt.Sprintf("🆚【%s】同比数据未找到\n", appName)
|
||||
}
|
||||
return text, title, descript
|
||||
}
|
||||
|
||||
// compareMetric 核心对比指标。数据机器人汇报末尾的"核心对比"块用,只挑最常看的几项;
|
||||
// 全量指标在上面各段落里已经逐行带了同比涨跌,这里不重复。
|
||||
type compareMetric struct {
|
||||
Name string
|
||||
IsMoney bool // true 表示底层值的单位是"分",展示前要除以 100
|
||||
Pick func(*BotHourStat) int64
|
||||
}
|
||||
|
||||
var compareMetrics = []compareMetric{
|
||||
{Name: "当日充值", IsMoney: true, Pick: func(s *BotHourStat) int64 { return s.TodayStat.Recharge.Value() }},
|
||||
{Name: "当日新增", Pick: func(s *BotHourStat) int64 { return s.TodayStat.Register.Count() }},
|
||||
{Name: "当日日活", Pick: func(s *BotHourStat) int64 { return s.TodayStat.Visit.Count() }},
|
||||
{Name: "小时充值", IsMoney: true, Pick: func(s *BotHourStat) int64 { return s.HourStat.Recharge.Value() }},
|
||||
{Name: "小时新增", Pick: func(s *BotHourStat) int64 { return s.HourStat.Register.Count() }},
|
||||
}
|
||||
|
||||
// weekdayCN 星期的中文名,用来拼"周三"这种基准时间标签
|
||||
var weekdayCN = [...]string{"日", "一", "二", "三", "四", "五", "六"}
|
||||
|
||||
// formatRate 把万分比涨跌渲染成带箭头的百分数,箭头用法和 LineCount 保持一致。
|
||||
// 这里按万分比算而不是沿用 LineCount 的千分比: 千分比只能给出一位小数,
|
||||
// 23.45% 会被截成 23.40%,而这一块就是专门拿来看涨跌的。
|
||||
func formatRate(bp int64) string {
|
||||
v := float64(bp) / 100
|
||||
switch {
|
||||
case v < 0:
|
||||
return fmt.Sprintf("⬇%.2f%%", v)
|
||||
case v > 0:
|
||||
return fmt.Sprintf("⬆+%.2f%%", v)
|
||||
default:
|
||||
return fmt.Sprintf("%.2f%%", v)
|
||||
}
|
||||
}
|
||||
|
||||
// divisor 该指标展示时的除数
|
||||
func (m compareMetric) divisor() int64 {
|
||||
if m.IsMoney {
|
||||
return 100
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// compareCell 渲染一个指标相对某一期基准的涨跌幅。
|
||||
// base 为 nil 表示那一期的记录没查到;基准为 0 时算不出涨跌幅,
|
||||
// 写 -- 而不是一个看起来正常的 0.00%。
|
||||
func compareCell(label string, m compareMetric, curr int64, base *BotHourStat) string {
|
||||
if base == nil {
|
||||
return fmt.Sprintf("%s:无数据", label)
|
||||
}
|
||||
baseValue := m.Pick(base)
|
||||
if baseValue == 0 {
|
||||
return fmt.Sprintf("%s:--", label)
|
||||
}
|
||||
bp := (curr - baseValue) * 10000 / baseValue
|
||||
return fmt.Sprintf("%s:%s", label, formatRate(bp))
|
||||
}
|
||||
|
||||
// hourDesc 把小时对齐的时点写成"11-14 周六 15点"
|
||||
func hourDesc(t time.Time) string {
|
||||
return fmt.Sprintf("%s 周%s %d点", t.Format("01-02"), weekdayCN[t.Weekday()], t.Hour())
|
||||
}
|
||||
|
||||
// compareDesc 基准时间的可读描述。不重复写小时(两期基准都跟当日同一个小时),
|
||||
// 但保留星期: 环比是整七天前,星期几必然和当日一致,写出来才能一眼看出没错位。
|
||||
// 记录没查到时直接在标签上标出来,免得看到一排"无数据"还要猜是哪天的。
|
||||
func compareDesc(want time.Time, got *BotHourStat) string {
|
||||
desc := fmt.Sprintf("%s 周%s", want.Format("01-02"), weekdayCN[want.Weekday()])
|
||||
if got == nil {
|
||||
return desc + "(无数据)"
|
||||
}
|
||||
return desc
|
||||
}
|
||||
|
||||
// DisplayCompareText 汇报末尾的"核心对比"块。
|
||||
// 业务口径: 同比 = 对比昨天同一小时, 环比 = 对比上周当日同一小时。
|
||||
// 这个叫法和财务上的习惯正好相反,是按需求方的要求定的,不要"纠正"过来。
|
||||
// 两期基准都拿不到时返回空串,不往汇报里塞一块全是"无数据"的内容。
|
||||
func (s *BotHourStat) DisplayCompareText(index string, yesterday, lastWeek *BotHourStat) string {
|
||||
if s == nil || (yesterday == nil && lastWeek == nil) {
|
||||
return ""
|
||||
}
|
||||
text := fmt.Sprintf("%s核心对比:\n", index)
|
||||
text += fmt.Sprintf("┣当日:%s\n", hourDesc(s.SumDate))
|
||||
text += fmt.Sprintf("┣基准:环比 %s / 同比 %s\n",
|
||||
compareDesc(s.SumDate.AddDate(0, 0, -7), lastWeek),
|
||||
compareDesc(s.SumDate.AddDate(0, 0, -1), yesterday))
|
||||
for _, m := range compareMetrics {
|
||||
curr := m.Pick(s)
|
||||
text += fmt.Sprintf("┣%s:%d %s %s\n", m.Name, curr/m.divisor(),
|
||||
compareCell("环比", m, curr, lastWeek),
|
||||
compareCell("同比", m, curr, yesterday))
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
+22
-2
@@ -94,7 +94,7 @@ func DailyBotReportTask(ctx context.Context, bot *tgbot.TgBot, appId int32, repo
|
||||
//text := report.DisplayDayHourText(appName, report)
|
||||
//_ = bot.SendBroadcastMessage(text, "", tgbot.FileTypeNone)
|
||||
|
||||
text, title, desc := report.DisplayDayHourTextWithTitle(appName, historyReport)
|
||||
text, title, desc := report.DisplayDayHourTextWithTitle(appName, historyReport, lastWeekHourStat(ctx, appId, reportTime))
|
||||
et := tgbot.NewEntityList(title, desc, []*tgbot.Entity{
|
||||
{
|
||||
Content: text,
|
||||
@@ -144,7 +144,7 @@ func DailyBotReportInfo(ctx context.Context, appId int32, reportTime time.Time)
|
||||
appNameMap, _ := appInfoMod.GetAllAppIdNameMap(ctx)
|
||||
appName := appNameMap[appId]
|
||||
if statg.GlobalEndpoints.GetBotStatEndpoints().ExistsByAppId(appId) {
|
||||
text, title, desc = report.DisplayDayHourTextWithTitle(appName, historyReport)
|
||||
text, title, desc = report.DisplayDayHourTextWithTitle(appName, historyReport, lastWeekHourStat(ctx, appId, reportTime))
|
||||
}
|
||||
log.Error("DailyBotReportTask end", log.Any("appid", appId),
|
||||
log.Any("time", reportTime))
|
||||
@@ -347,3 +347,23 @@ func BotDailyHourStat(ctx context.Context, appId int32, reportTime time.Time, sa
|
||||
}
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// lastWeekHourStat 取上周当日同一小时的统计记录,给汇报里的"环比"做基准。
|
||||
// 只查不补算: 现算一份七天前的数据要把当天的原始流水全量聚合一遍,代价高,
|
||||
// 而且历史原始数据可能已经被清理,算出来反而是错的。查不到就返回 nil,
|
||||
// 展示层会把那一行标成"无数据"。
|
||||
func lastWeekHourStat(ctx context.Context, appId int32, reportTime time.Time) *botdailyhourstat.BotHourStat {
|
||||
filter := bson.M{
|
||||
botreport.FN.BotReport.AppId: appId,
|
||||
botreport.FN.BotReport.SumDate: reportTime.AddDate(0, 0, -7),
|
||||
}
|
||||
report, err := botdailyhourstat.FindOneBotHourStatEx(ctx, filter)
|
||||
if err != nil {
|
||||
if err != mongo.ErrNoDocuments {
|
||||
log.Warn("lastWeekHourStat failed", log.E(err),
|
||||
log.Any("appId", appId), log.Any("sumDate", reportTime))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return report
|
||||
}
|
||||
|
||||
@@ -491,7 +491,7 @@ func DailyStat(ctx context.Context, cmd *tgbot.Command, moveBackDays int) error
|
||||
return tgbot.NewUserError(cmd, "服务器错误:"+err.Error())
|
||||
}
|
||||
}
|
||||
text, title, desc := report.DisplayDayHourTextWithTitle(appInfo.Name, historyReport)
|
||||
text, title, desc := report.DisplayDayHourTextWithTitle(appInfo.Name, historyReport, lastWeekHourStat(ctx, appId, reportTime))
|
||||
et := tgbot.NewEntityList(title, desc, []*tgbot.Entity{
|
||||
{
|
||||
Content: text,
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
downloadinfo "stat/models/downloadinfo"
|
||||
"stat/models/versionMod"
|
||||
"stat/service/cacheSer"
|
||||
"stat/service/cpcDownloadCfgSer"
|
||||
"stat/service/crossSer"
|
||||
"stat/statg"
|
||||
"strings"
|
||||
@@ -189,6 +190,18 @@ func PageButtonClick(ctx context.Context, userAgent string, peerIP string, req *
|
||||
return
|
||||
}
|
||||
|
||||
// 双端下载配置里给该渠道配了 Android 下载地址,就用它覆盖 link 并直接返回。
|
||||
// link 也改: 只读 data.link 的老客户端(useAppDownload.ts)才能用上渠道包;
|
||||
// 客户端取值是 links[index] || links[0],单元素数组任意下标都能回落。
|
||||
// 必须在批量打包逻辑之前返回: 下面那段会按默认域名重算并覆盖 link。
|
||||
if req.Terminal == "android" && channelCode.DC != "" {
|
||||
if packUrl := channelApkDownUrl(ctx, req.AppId, channelCode.DC); packUrl != "" {
|
||||
resp.ChannelApkAndDownUrl = packUrl
|
||||
resp.Link = []string{packUrl}
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 新增批量打包逻辑
|
||||
if req.Terminal == "android" && len(data.Md5) > 0 {
|
||||
randTime := int64(60*60) + int64(rand.Intn(60*60))
|
||||
@@ -273,6 +286,22 @@ func PageButtonClick(ctx context.Context, userAgent string, peerIP string, req *
|
||||
return
|
||||
}
|
||||
|
||||
// channelApkDownUrl 取双端下载配置里该渠道的 Android 下载地址,只用于落地页按钮。
|
||||
// 配置里有一部分填的是落地页地址(形如 https://xxx?dc=blt0001)而不是 apk 包,
|
||||
// 落地页拿到它会再跳一次落地页,所以带 dc 参数的一律当没配,返回空串让调用方走默认下载链接。
|
||||
// 不能直接改 cpcDownloadCfgSer.GetDownloadUrl: /download/{appId}/{channel} 这条双端路由
|
||||
// 依赖它跳落地页,过滤掉会把那个功能弄坏。
|
||||
func channelApkDownUrl(ctx context.Context, appId int32, dc string) string {
|
||||
url := cpcDownloadCfgSer.GetDownloadUrl(ctx, appId, dc)
|
||||
if url == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.Contains(url, "?dc=") || strings.Contains(url, "&dc=") {
|
||||
return ""
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
// PageButtonList 落地页获取下载按钮
|
||||
func PageButtonList(ctx context.Context, isIos bool, peerIP string, req *proto.PageButtonListReq) (resp *proto.PageButtonListResp, err error) {
|
||||
resp = &proto.PageButtonListResp{
|
||||
@@ -377,5 +406,10 @@ func PageButtonList(ctx context.Context, isIos bool, peerIP string, req *proto.P
|
||||
resp.HasChannelApk = true
|
||||
}
|
||||
|
||||
// 双端下载配置里给该渠道配了 Android 下载地址就一并返回,落地页优先用它
|
||||
if !isIos && channelCode.DC != "" {
|
||||
resp.ChannelApkAndDownUrl = channelApkDownUrl(ctx, req.AppId, channelCode.DC)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user