Files
junhong_cmp_fiber/internal/domain/operationsreport/metrics.go
break 5e78809b93
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 15m33s
feat(运营报表): AUG26-015 设备激活与套餐续费日报快照、查询趋势与受控导出
- 新增成对迁移 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
2026-09-18 09:42:28 +08:00

151 lines
5.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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)
}