package operationsreport import ( "context" "strconv" "strings" "time" domainreport "github.com/break/junhong_cmp_fiber/internal/domain/operationsreport" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" "github.com/break/junhong_cmp_fiber/pkg/errors" ) // groupKey 是一个分组行在结果中的唯一键:value 是展示值,identity 是实体键。 // 同一展示值下的不同实体不会被合并(例如两个店铺同名为「未设置」仍各占一行)。 type groupKey struct { value string identity string } // activationMetrics 是一行(头行或分组聚合)的激活指标原始值。 type activationMetrics struct { PurchasedDeviceCount int64 ActivatedDeviceCount int64 OnlineDeviceCount int64 ActiveDeviceCount int64 TotalRealTrafficMB float64 } // metricsFromHead 由快照头行构造激活指标原始值。 func metricsFromHead(head model.OperationsReportSnapshot) activationMetrics { return activationMetrics{ PurchasedDeviceCount: head.PurchasedDeviceCount, ActivatedDeviceCount: head.ActivatedDeviceCount, OnlineDeviceCount: head.OnlineDeviceCount, ActiveDeviceCount: head.ActiveDeviceCount, TotalRealTrafficMB: head.TotalRealTrafficMB, } } // item 按报表口径组装一行激活指标。 // 分母为零的比率与卡均为空值;累计用量按 1 GB = 1024 MB 折算; // 预测卡均的当月口径取所选结束日所在上海自然月。 func (m activationMetrics) item(groupValue string, endDay time.Time) dto.OperationsActivationSummaryItem { purchased := m.PurchasedDeviceCount activated := m.ActivatedDeviceCount online := m.OnlineDeviceCount active := m.ActiveDeviceCount item := dto.OperationsActivationSummaryItem{ GroupValue: groupValue, PurchasedDeviceCount: &purchased, ActivatedDeviceCount: &activated, OnlineDeviceCount: &online, ActiveDeviceCount: &active, } if rate, ok := domainreport.Ratio(activated, purchased); ok { item.ActivationRate = &rate } trafficGB := domainreport.Round2(m.TotalRealTrafficMB / domainreport.MBPerGB) item.TotalRealTrafficGB = &trafficGB if average, ok := domainreport.CardAverageGB(m.TotalRealTrafficMB, online); ok { item.PerUserAverageGB = &average } if forecast, ok := domainreport.ForecastCardAverageGB(m.TotalRealTrafficMB, online, endDay); ok { item.ForecastAverageIncludingZeroGB = &forecast } if forecast, ok := domainreport.ForecastCardAverageGB(m.TotalRealTrafficMB, active, endDay); ok { item.ForecastAverageExcludingZeroGB = &forecast } return item } // renewalMetrics 是一组(头行或分组聚合)的到期与续费资产数。 type renewalMetrics struct { DueAssets int64 RenewedAssets int64 } // renewal 按报表口径组装一行续费指标。 // 续费率分母为零时为空值;新增未续费数为到期数减续费数,不小于零。 func (m renewalMetrics) item(groupValue string) dto.OperationsRenewalSummaryItem { due := m.DueAssets renewed := m.RenewedAssets unrenewed := due - renewed if unrenewed < 0 { unrenewed = 0 } item := dto.OperationsRenewalSummaryItem{ GroupValue: groupValue, DueAssetCount: &due, RenewedAssetCount: &renewed, NewUnrenewedAssetCount: &unrenewed, } if rate, ok := domainreport.RenewalRate(renewed, due); ok { item.RenewalRate = &rate } return item } // renewalMetricsFromHead 由快照头行构造续费指标原始值。 func renewalMetricsFromHead(head model.OperationsReportSnapshot) renewalMetrics { return renewalMetrics{DueAssets: head.RenewalDueAssetCount, RenewedAssets: head.RenewalRenewedAssetCount} } // aggregateKeyRow 是一次分组聚合查询的原始投影。 // 只填充当前维度与当前报表实际选择的列,其余列保持零值。 type aggregateKeyRow struct { Period string `gorm:"column:period"` SnapshotDay string `gorm:"column:snapshot_day"` DeviceName string `gorm:"column:device_name"` DeviceModel string `gorm:"column:device_model"` Manufacturer string `gorm:"column:manufacturer"` BusinessUserGroupID *uint `gorm:"column:business_user_group_id"` BusinessUserGroupName string `gorm:"column:business_user_group_name"` AgentAccountID *uint `gorm:"column:agent_account_id"` AgentAccountName string `gorm:"column:agent_account_name"` RootShopID *uint `gorm:"column:root_shop_id"` RootShopName string `gorm:"column:root_shop_name"` ShopID *uint `gorm:"column:shop_id"` ShopName string `gorm:"column:shop_name"` BusinessOwnerAccountID *uint `gorm:"column:business_owner_account_id"` BusinessOwnerName string `gorm:"column:business_owner_name"` SeriesID *uint `gorm:"column:series_id"` SeriesName string `gorm:"column:series_name"` PackageName string `gorm:"column:package_name"` PurchasedDeviceCount int64 `gorm:"column:purchased_device_count"` ActivatedDeviceCount int64 `gorm:"column:activated_device_count"` OnlineDeviceCount int64 `gorm:"column:online_device_count"` ActiveDeviceCount int64 `gorm:"column:active_device_count"` TotalRealTrafficMB float64 `gorm:"column:total_real_traffic_mb"` DueAssets int64 `gorm:"column:due_assets"` RenewedAssets int64 `gorm:"column:renewed_assets"` } // activationKeyColumns 返回激活情况维度在快照行上的分组键列。 // 实体 ID 列一并进入分组键,避免同展示名不同实体被合并。 func activationKeyColumns(dimension string) []string { switch dimension { case domainreport.DimensionDeviceName: return []string{"device_name"} case domainreport.DimensionDeviceModel: return []string{"device_model"} case domainreport.DimensionManufacturer: return []string{"manufacturer"} case domainreport.DimensionBusinessUserGroup: return []string{"business_user_group_id", "business_user_group_name"} case domainreport.DimensionAgent: return []string{"agent_account_id", "agent_account_name", "root_shop_id", "root_shop_name"} case domainreport.DimensionShop: return []string{"shop_id", "shop_name"} case domainreport.DimensionBusinessOwner: return []string{"business_owner_account_id", "business_owner_name"} default: return nil } } // renewalKeyColumns 返回套餐续费维度在快照行上的分组键列。 func renewalKeyColumns(dimension string) []string { switch dimension { case domainreport.DimensionPackageSeries: return []string{"series_id", "series_name"} case domainreport.DimensionPackageName: return []string{"package_name"} case domainreport.DimensionBusinessUserGroup: return []string{"business_user_group_id", "business_user_group_name"} case domainreport.DimensionAgent: return []string{"agent_account_id", "agent_account_name", "root_shop_id", "root_shop_name"} case domainreport.DimensionShop: return []string{"shop_id", "shop_name"} case domainreport.DimensionBusinessOwner: return []string{"business_owner_account_id", "business_owner_name"} default: return nil } } // groupKeyFor 由聚合行与维度编码推导分组键。 // // 「代理」维度映射为单一取值:优先取代理账号(归属一级代理店铺的 user_type=3 账号), // 账号缺失时退回一级代理店铺名称;两者都缺失时为固定占位。 func groupKeyFor(dimension string, row aggregateKeyRow) groupKey { switch dimension { case domainreport.DimensionDeviceName: return textKey("device_name", row.DeviceName) case domainreport.DimensionDeviceModel: return textKey("device_model", row.DeviceModel) case domainreport.DimensionManufacturer: return textKey("manufacturer", row.Manufacturer) case domainreport.DimensionBusinessUserGroup: return idKey("business_user_group", formatOptionalUint(row.BusinessUserGroupID), row.BusinessUserGroupName) case domainreport.DimensionAgent: if row.AgentAccountID != nil { return groupKey{ value: domainreport.TextOrPlaceholder(row.AgentAccountName), identity: "agent_account:" + strconv.FormatUint(uint64(*row.AgentAccountID), 10), } } if row.RootShopID != nil { return groupKey{ value: domainreport.TextOrPlaceholder(row.RootShopName), identity: "agent_shop:" + strconv.FormatUint(uint64(*row.RootShopID), 10), } } return groupKey{value: domainreport.PlaceholderUnset, identity: "agent:none"} case domainreport.DimensionShop: return idKey("shop", formatOptionalUint(row.ShopID), row.ShopName) case domainreport.DimensionBusinessOwner: return idKey("business_owner", formatOptionalUint(row.BusinessOwnerAccountID), row.BusinessOwnerName) case domainreport.DimensionPackageSeries: return idKey("package_series", formatOptionalUint(row.SeriesID), row.SeriesName) case domainreport.DimensionPackageName: return textKey("package_name", row.PackageName) default: return groupKey{value: domainreport.DimensionAll} } } // textKey 以文本值本身作为实体键。 func textKey(prefix, value string) groupKey { return groupKey{value: domainreport.TextOrPlaceholder(value), identity: prefix + ":" + value} } // idKey 以实体 ID 作为实体键,ID 缺失时退回文本值。 func idKey(prefix, id, value string) groupKey { if id == "" { return groupKey{value: domainreport.TextOrPlaceholder(value), identity: prefix + ":name:" + value} } return groupKey{value: domainreport.TextOrPlaceholder(value), identity: prefix + ":" + id} } // activationDayGroups 按(快照日,分组维度)聚合激活指标。 // 只读设备激活快照行,不回查实时事实;数据范围按快照行的店铺列施加。 func (q *Query) activationDayGroups(ctx context.Context, days []time.Time, dimension string, scope []uint) (map[string]map[groupKey]activationMetrics, error) { result := make(map[string]map[groupKey]activationMetrics, len(days)) if len(days) == 0 { return result, nil } selects := []string{"to_char(snapshot_date, 'YYYY-MM-DD') AS snapshot_day"} groupColumns := []string{"snapshot_date"} for _, column := range activationKeyColumns(dimension) { selects = append(selects, column) groupColumns = append(groupColumns, column) } selects = append(selects, activationAggregateSelects()...) query := q.db.WithContext(ctx).Table("tb_operations_report_activation_row"). Select(strings.Join(selects, ", ")). Where("snapshot_date IN ?", uniqueDays(days)). Group(strings.Join(groupColumns, ", ")) if len(scope) > 0 { query = query.Where("shop_id IN ?", scope) } var rows []aggregateKeyRow if err := query.Scan(&rows).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "聚合运营报表设备激活快照行失败") } for _, row := range rows { day := row.SnapshotDay if _, exists := result[day]; !exists { result[day] = make(map[groupKey]activationMetrics) } result[day][groupKeyFor(dimension, row)] = activationMetrics{ PurchasedDeviceCount: row.PurchasedDeviceCount, ActivatedDeviceCount: row.ActivatedDeviceCount, OnlineDeviceCount: row.OnlineDeviceCount, ActiveDeviceCount: row.ActiveDeviceCount, TotalRealTrafficMB: row.TotalRealTrafficMB, } } return result, nil } // activationAggregateSelects 返回激活指标的聚合表达式。 func activationAggregateSelects() []string { return []string{ "COALESCE(SUM(CASE WHEN purchased THEN 1 ELSE 0 END), 0) AS purchased_device_count", "COALESCE(SUM(CASE WHEN realnamed THEN 1 ELSE 0 END), 0) AS activated_device_count", "COALESCE(SUM(CASE WHEN online THEN 1 ELSE 0 END), 0) AS online_device_count", "COALESCE(SUM(CASE WHEN active THEN 1 ELSE 0 END), 0) AS active_device_count", "COALESCE(SUM(real_traffic_mb), 0)::float8 AS total_real_traffic_mb", } } // renewalsAggregate 聚合续费指标(到期与续费均按资产去重)。 // // periodExpr 为空表示整段合计(汇总查询);否则按其分组,键与 domain.FormatPeriod 一致。 // 期内的资产去重在 SQL 层完成,因此按月趋势的同一资产多次到期只计一次。 func (q *Query) renewalsAggregate(ctx context.Context, lower, upper *time.Time, periodExpr, dimension string, scope []uint) (map[string]map[groupKey]renewalMetrics, error) { selects := make([]string, 0, 4) groupColumns := make([]string, 0, 4) if periodExpr != "" { selects = append(selects, periodExpr+" AS period") groupColumns = append(groupColumns, periodExpr) } for _, column := range renewalKeyColumns(dimension) { selects = append(selects, column) groupColumns = append(groupColumns, column) } selects = append(selects, "COUNT(DISTINCT asset_type || ':' || asset_id) AS due_assets", "COUNT(DISTINCT asset_type || ':' || asset_id) FILTER (WHERE renewed) AS renewed_assets") query := q.db.WithContext(ctx).Table("tb_operations_report_renewal_row").Select(strings.Join(selects, ", ")) if lower != nil { query = query.Where("snapshot_date >= ?::date", domainreport.FormatSnapshotDay(*lower)) } if upper != nil { query = query.Where("snapshot_date <= ?::date", domainreport.FormatSnapshotDay(*upper)) } if len(scope) > 0 { query = query.Where("shop_id IN ?", scope) } if len(groupColumns) > 0 { query = query.Group(strings.Join(groupColumns, ", ")) } var rows []aggregateKeyRow if err := query.Scan(&rows).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "聚合运营报表套餐续费快照行失败") } result := make(map[string]map[groupKey]renewalMetrics, len(rows)) if periodExpr == "" { result[""] = make(map[groupKey]renewalMetrics) } for _, row := range rows { key := groupKeyFor(dimension, row) if !rowHasGroup(row, dimension) { key = groupKey{value: domainreport.DimensionAll} } if _, exists := result[row.Period]; !exists { result[row.Period] = make(map[groupKey]renewalMetrics) } result[row.Period][key] = renewalMetrics{DueAssets: row.DueAssets, RenewedAssets: row.RenewedAssets} } return result, nil } // rowHasGroup 判断聚合行是否携带分组维度取值(未分组时为 false)。 func rowHasGroup(row aggregateKeyRow, dimension string) bool { switch dimension { case domainreport.DimensionPackageSeries: return row.SeriesID != nil || row.SeriesName != "" case domainreport.DimensionPackageName: return row.PackageName != "" case domainreport.DimensionBusinessUserGroup: return row.BusinessUserGroupID != nil || row.BusinessUserGroupName != "" case domainreport.DimensionAgent: return row.AgentAccountID != nil || row.RootShopID != nil || row.RootShopName != "" case domainreport.DimensionShop: return row.ShopID != nil || row.ShopName != "" case domainreport.DimensionBusinessOwner: return row.BusinessOwnerAccountID != nil || row.BusinessOwnerName != "" default: return false } } // formatOptionalUint 输出可空 ID 的文本形式。 func formatOptionalUint(value *uint) string { if value == nil { return "" } return strconv.FormatUint(uint64(*value), 10) } // periodExpr 返回趋势期在 SQL 中的表达式,键与 domain.FormatPeriod 一致。 func periodExpr(granularity string) string { if granularity == domainreport.GranularityMonth { return "to_char(snapshot_date, 'YYYY-MM')" } return "to_char(snapshot_date, 'YYYY-MM-DD')" }