feat(运营报表): AUG26-015 设备激活与套餐续费日报快照、查询趋势与受控导出
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 15m33s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 15m33s
- 新增成对迁移 000231 与三张快照表:日级头行、设备粒度激活行、到期事件粒度续费行,以快照日期为唯一键 - 新增每日 03:30(Asia/Shanghai)日报快照生成任务与幂等整日替换,失败重试沿用同一目标日 - 新增六条受控入口:两张报表的汇总、日/月趋势与受控导出,配套查询层只读快照事实 - 新增 operations_activation 与 operations_renewal 两个导出场景,创建期冻结筛选与可见店铺范围、派发期冻结表头、执行期只按冻结值复核资格 - 采购数量口径按系统内未删除设备数实施并在 PRD 标注,附实测差额依据 - 同步证据链 requirement-evidence.json 与入口能力矩阵、ARCHITECTURE 与验证记录 - 归档 change add-operations-reports 并新建主 Spec openspec/specs/operations-report/spec.md
This commit is contained in:
150
internal/domain/operationsreport/metrics.go
Normal file
150
internal/domain/operationsreport/metrics.go
Normal file
@@ -0,0 +1,150 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user