package operationsreport import ( "context" "time" domainreport "github.com/break/junhong_cmp_fiber/internal/domain/operationsreport" ) // trendPeriod 是趋势的一个期:期标识、期末快照日与(按月粒度时)期内全部快照日。 type trendPeriod struct { Key string RepresentativeDay time.Time Days []time.Time } // activationPeriods 返回激活情况趋势的期序列(升序)。 // // 按日粒度的期是该区间内的每个快照日;按月粒度的期是每个自然月,期末取该月最后一个快照日。 // 无快照的期不出现在结果中。 func (q *Query) activationPeriods(ctx context.Context, granularity string, start, end *time.Time) ([]trendPeriod, error) { if granularity == domainreport.GranularityDay { days, err := q.matchedDays(ctx, lowerDay(start), upperDay(end)) if err != nil { return nil, err } periods := make([]trendPeriod, 0, len(days)) for _, day := range days { periods = append(periods, trendPeriod{ Key: domainreport.FormatPeriod(granularity, day), RepresentativeDay: day, Days: []time.Time{day}, }) } return periods, nil } lower, upper := monthSpan(start, end) days, err := q.matchedDays(ctx, lower, upper) if err != nil { return nil, err } return groupMonthPeriods(granularity, days, lowerDay(start), upperDay(end)), nil } // previousPeriodRepresentatives 返回每个期的前一期期末快照日;前一期无快照的期不出现在结果中。 // // 前一期指日历上的上一自然日或上一自然月,即使它早于请求区间起点也必须读取: // 新增类指标按「期末 − 前一期期末」计算,任一侧无快照时为空。 func (q *Query) previousPeriodRepresentatives(ctx context.Context, granularity string, periods []trendPeriod) (map[string]time.Time, error) { result := make(map[string]time.Time, len(periods)) if len(periods) == 0 { return result, nil } earliest := domainreport.PeriodOf(granularity, periods[0].RepresentativeDay) for _, period := range periods { start := domainreport.PeriodOf(granularity, period.RepresentativeDay) if start.Before(earliest) { earliest = start } } searchFrom := domainreport.PreviousPeriodStart(granularity, earliest) days, err := q.matchedDays(ctx, &searchFrom, nil) if err != nil { return nil, err } for _, period := range periods { periodStart := domainreport.PeriodOf(granularity, period.RepresentativeDay) previousStart := domainreport.PreviousPeriodStart(granularity, periodStart) previousEnd := previousStart if granularity == domainreport.GranularityMonth { previousEnd = endOfMonth(previousStart) } if representative, ok := lastDayWithin(days, previousStart, previousEnd); ok { result[period.Key] = representative } } return result, nil } // monthSpan 把区间换算为覆盖整月的日期范围,供按月趋势读取所需的快照日集合。 func monthSpan(start, end *time.Time) (*time.Time, *time.Time) { var lower, upper *time.Time if start != nil { day := domainreport.LowerBoundDay(*start) first := domainreport.PeriodOf(domainreport.GranularityMonth, day) lower = &first } if end != nil { day := domainreport.UpperBoundDay(*end) last := endOfMonth(domainreport.PeriodOf(domainreport.GranularityMonth, day)) upper = &last } return lower, upper } // groupMonthPeriods 把快照日按月聚合为趋势期。 // 期末快照日落在请求区间之外的月份不作为期出现(其快照日仅用于推断前一期)。 func groupMonthPeriods(granularity string, days []time.Time, lower, upper *time.Time) []trendPeriod { order := make([]string, 0, len(days)) buckets := make(map[string][]time.Time, len(days)) for _, day := range days { key := domainreport.FormatPeriod(granularity, day) if _, exists := buckets[key]; !exists { order = append(order, key) } buckets[key] = append(buckets[key], day) } periods := make([]trendPeriod, 0, len(order)) for _, key := range order { bucket := buckets[key] representative := bucket[len(bucket)-1] if lower != nil && representative.Before(*lower) { continue } if upper != nil && representative.After(*upper) { continue } periods = append(periods, trendPeriod{Key: key, RepresentativeDay: representative, Days: bucket}) } return periods } // endOfMonth 返回该月最后一天(上海自然日)。 func endOfMonth(monthStart time.Time) time.Time { return time.Date(monthStart.Year(), monthStart.Month()+1, 0, 0, 0, 0, 0, domainreport.ShanghaiLocation()) } // lastDayWithin 返回闭区间内最后一个存在的快照日。 func lastDayWithin(days []time.Time, from, to time.Time) (time.Time, bool) { var found time.Time ok := false for _, day := range days { if day.Before(from) || day.After(to) { continue } found = day ok = true } return found, ok }