feat(运营报表): AUG26-015 设备激活与套餐续费日报快照、查询趋势与受控导出
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:
2026-09-18 09:42:28 +08:00
parent 6333f4ad13
commit 5e78809b93
46 changed files with 5580 additions and 101 deletions

View 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)
}