// Package operationsreport 是运营报表(设备激活与套餐续费)的口径域。 // // 本包只表达可复现的口径与纯计算:真流量换算、比率与卡均、分母为零语义、 // 预测卡均的当月口径、采购数量口径入口与续费判定规则。 // 不依赖 Fiber、GORM、Redis、Asynq 或任何外部 SDK,也不做任何读写。 package operationsreport import ( "math" "time" ) // MBPerGB 是报表域自持的流量换算常量:1 GB = 1024 MB。 // 与 internal/domain/carrierthreshold 的换算同值同源;不为一个换算常数建立跨域依赖。 const MBPerGB = 1024 // shanghaiLocation 是报表口径使用的上海时区(固定 +08:00)。 var shanghaiLocation = time.FixedZone("Asia/Shanghai", 8*60*60) // ShanghaiLocation 返回报表口径使用的上海时区。 func ShanghaiLocation() *time.Location { return shanghaiLocation } // SnapshotDay 把任意时刻归一为它所在的上海自然日零点。 // 报表的一切跨日比较都使用上海自然日,不使用服务器本地时区。 func SnapshotDay(value time.Time) time.Time { local := value.In(shanghaiLocation) return time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, shanghaiLocation) } // PreviousDay 返回给定上海自然日的前一自然日零点。 func PreviousDay(day time.Time) time.Time { return SnapshotDay(day).AddDate(0, 0, -1) } // ParseSnapshotDay 解析 yyyy-MM-dd 形式的上海自然日。 func ParseSnapshotDay(value string) (time.Time, error) { parsed, err := time.ParseInLocation("2006-01-02", value, shanghaiLocation) if err != nil { return time.Time{}, err } return parsed, nil } // FormatSnapshotDay 输出上海自然日的 yyyy-MM-dd 文本。 func FormatSnapshotDay(day time.Time) string { return SnapshotDay(day).Format("2006-01-02") } // FormatMonthPeriod 输出上海自然月的 yyyy-MM 文本。 func FormatMonthPeriod(day time.Time) string { return SnapshotDay(day).Format("2006-01") } // LowerBoundDay 按落界规则返回区间起点入选的最早快照日期。 // // 落界规则(设计 D10):快照日期 D 入选,当且仅当 D 的零点(+08:00)落在请求区间内。 // 因此起点恰好落在零点时当日入选,否则从次日起入选。 func LowerBoundDay(start time.Time) time.Time { day := SnapshotDay(start) if start.After(day) { return day.AddDate(0, 0, 1) } return day } // UpperBoundDay 按落界规则返回区间终点入选的最晚快照日期。 func UpperBoundDay(end time.Time) time.Time { return SnapshotDay(end) } // PeriodOf 返回给定快照日期所属的趋势期标识:按日为上海自然日,按月为该月首日。 func PeriodOf(granularity string, day time.Time) time.Time { normalized := SnapshotDay(day) if granularity == GranularityMonth { return time.Date(normalized.Year(), normalized.Month(), 1, 0, 0, 0, 0, shanghaiLocation) } return normalized } // FormatPeriod 输出趋势期标识:按日为 yyyy-MM-dd,按月为 yyyy-MM。 func FormatPeriod(granularity string, day time.Time) string { if granularity == GranularityMonth { return FormatMonthPeriod(day) } return FormatSnapshotDay(day) } // PreviousPeriodStart 返回给定期起始日所属期的前一期起始日(按日减一天,按月减一个月)。 func PreviousPeriodStart(granularity string, periodStart time.Time) time.Time { if granularity == GranularityMonth { return periodStart.AddDate(0, -1, 0) } return periodStart.AddDate(0, 0, -1) } // Ratio 计算比率并按两位小数取整;分母不大于零时不可计算,返回 false(空值语义)。 // 比率不设上限:设备删除或迁移可使激活率超过 100%,如实呈现。 func Ratio(numerator, denominator int64) (float64, bool) { if denominator <= 0 { return 0, false } return Round2(float64(numerator) / float64(denominator)), true } // CardAverageGB 计算卡均用量(GB):累计真流量折算 GB 后除以分母设备数。 // 分母不大于零时不可计算,返回 false(空值语义)。 func CardAverageGB(totalRealTrafficMB float64, denominator int64) (float64, bool) { if denominator <= 0 { return 0, false } average := totalRealTrafficMB / MBPerGB / float64(denominator) return Round2(average), true } // ForecastCardAverageGB 计算预测卡均(GB):先按卡均口径得出日均,再按结束日所在上海自然月年化。 // 「当月」= 所选结束日所在上海自然月;已过天数 = 结束日日期号;当月总天数 = 该月自然日数。 // 分母不大于零时不可计算,返回 false(空值语义)。 func ForecastCardAverageGB(totalRealTrafficMB float64, denominator int64, endDate time.Time) (float64, bool) { average, ok := CardAverageGB(totalRealTrafficMB, denominator) if !ok { return 0, false } elapsedDays, totalDays := MonthElapsedAndTotalDays(endDate) if elapsedDays <= 0 || totalDays <= 0 { return 0, false } return Round2(average * float64(totalDays) / float64(elapsedDays)), true } // MonthElapsedAndTotalDays 返回结束日所在上海自然月的已过天数与当月总天数。 // 已过天数按结束日的日期号取值(不区分当月剩余天数),当月总天数取该自然月的实际天数。 func MonthElapsedAndTotalDays(endDate time.Time) (int, int) { day := SnapshotDay(endDate) totalDays := time.Date(day.Year(), day.Month()+1, 0, 0, 0, 0, 0, shanghaiLocation).Day() return day.Day(), totalDays } // Round2 按两位小数四舍五入。 func Round2(value float64) float64 { return math.Round(value*100) / 100 } // RenewalRate 计算续费率:续费资产数除以到期资产数。 // 分母为零时不可计算,返回 false(空值语义,导出写「-」)。 // 分子为分母子集,因此续费率不超过 100% 由构造保证,不做任何截断或钳制。 func RenewalRate(renewed, due int64) (float64, bool) { return Ratio(renewed, due) }