package models import ( "strings" "time" ) var activityLocation = time.FixedZone("Asia/Shanghai", 8*60*60) const ( // 日与时刻之间留空格,避免「20日11点」粘连难读 ActivityTimeLayout = "2006年1月2日 15点04分05秒" // 历史数据可能无空格,解析时兼容 activityTimeLayoutLegacy = "2006年1月2日15点04分05秒" ActivityDateLayout = "2006-01-02" ) func CurrentActivityDate() string { return time.Now().In(activityLocation).Format(ActivityDateLayout) } func CurrentActivityTime() string { return FormatActivityTime(time.Now()) } func FormatActivityTime(t time.Time) string { return t.In(activityLocation).Format(ActivityTimeLayout) } func ParseActivityTime(value string) (time.Time, bool) { value = strings.TrimSpace(value) if value == "" { return time.Time{}, false } layouts := []string{ActivityTimeLayout, activityTimeLayoutLegacy, time.RFC3339Nano, time.RFC3339} for _, layout := range layouts { if parsed, err := time.ParseInLocation(layout, value, activityLocation); err == nil { return parsed.In(activityLocation), true } if parsed, err := time.Parse(layout, value); err == nil { return parsed.In(activityLocation), true } } return time.Time{}, false } func ActivityDate(value string) (string, bool) { parsed, ok := ParseActivityTime(value) if !ok { return "", false } return parsed.In(activityLocation).Format(ActivityDateLayout), true } func HasActivityDate(values []string, date string) bool { for _, value := range values { if parsedDate, ok := ActivityDate(value); ok && parsedDate == date { return true } } return false } func ActivitySummary(values []string, fallbackDate string) (days int, streak int, lastAt string) { dateSet := make(map[string]struct{}, len(values)) var latest time.Time hasLatest := false for _, value := range values { parsed, ok := ParseActivityTime(value) if !ok { continue } dateKey := parsed.In(activityLocation).Format(ActivityDateLayout) dateSet[dateKey] = struct{}{} if !hasLatest || parsed.After(latest) { latest = parsed hasLatest = true lastAt = FormatActivityTime(parsed) } } if len(dateSet) == 0 && strings.TrimSpace(fallbackDate) != "" { dateSet[strings.TrimSpace(fallbackDate)] = struct{}{} days = 1 streak = 1 return } days = len(dateSet) if !hasLatest { return } cursor := time.Date(latest.In(activityLocation).Year(), latest.In(activityLocation).Month(), latest.In(activityLocation).Day(), 0, 0, 0, 0, activityLocation) for { key := cursor.Format(ActivityDateLayout) if _, ok := dateSet[key]; !ok { break } streak++ cursor = cursor.AddDate(0, 0, -1) } return }