本文目录导读:

纯前端 JavaScript 实现(本地存储)
function calculateStreak(records) {
if (records.length === 0) return 0;
// 按日期降序排序
records.sort((a, b) => new Date(b.date) - new Date(a.date));
let streak = 1;
const today = new Date();
today.setHours(0, 0, 0, 0);
// 检查今天是否打卡
const lastRecord = new Date(records[0].date);
lastRecord.setHours(0, 0, 0, 0);
const diffDays = Math.floor((today - lastRecord) / (1000 * 60 * 60 * 24));
if (diffDays > 1) return 0; // 最后打卡超过1天,连续中断
// 计算连续天数
for (let i = 1; i < records.length; i++) {
const currentDate = new Date(records[i].date);
const prevDate = new Date(records[i - 1].date);
const diff = Math.floor((prevDate - currentDate) / (1000 * 60 * 60 * 24));
if (diff === 1) {
streak++;
} else {
break;
}
}
return streak;
}
// 使用示例
const records = [
{ date: '2024-01-10' },
{ date: '2024-01-09' },
{ date: '2024-01-08' },
{ date: '2024-01-06' } // 中断
];
console.log(calculateStreak(records)); // 输出: 3
后端 Python 实现
from datetime import datetime, timedelta
from typing import List, Dict
def calculate_streak(records: List[Dict]) -> int:
if not records:
return 0
# 提取日期并排序
dates = [datetime.strptime(r['date'], '%Y-%m-%d').date() for r in records]
dates.sort(reverse=True)
# 检查今天是否打卡
today = datetime.now().date()
if dates[0] == today:
streak = 1
elif dates[0] == today - timedelta(days=1):
streak = 1
else:
return 0 # 最后打卡不是今天或昨天
# 计算连续天数
for i in range(1, len(dates)):
diff = (dates[i-1] - dates[i]).days
if diff == 1:
streak += 1
else:
break
return streak
# 使用示例
records = [
{'date': '2024-01-10'},
{'date': '2024-01-09'},
{'date': '2024-01-08'},
{'date': '2024-01-07'}
]
print(calculate_streak(records)) # 输出: 4
数据库查询(SQL)
-- 假设打卡记录表: check_ins(user_id, check_date)
-- 计算用户连续打卡天数
WITH daily_check AS (
SELECT
user_id,
check_date,
LAG(check_date) OVER (PARTITION BY user_id ORDER BY check_date DESC) as prev_date
FROM check_ins
WHERE user_id = 123
),
streak_calc AS (
SELECT
*,
CASE
WHEN prev_date IS NULL THEN 1
WHEN DATEDIFF(prev_date, check_date) = 1 THEN 0
ELSE 1
END as is_break
FROM daily_check
ORDER BY check_date DESC
)
SELECT COUNT(*) as streak_days
FROM streak_calc
WHERE is_break = 0
LIMIT 1;
考虑特殊情况的增强版本
function calculateEnhancedStreak(records, options = {}) {
const {
allowTodayGap = true, // 今天未打卡是否中断
includeWeekends = true, // 是否包含周末
maxGap = 0 // 允许的最大间隔天数
} = options;
if (records.length === 0) return { streak: 0, isActive: false };
// 排序并去重
const dates = [...new Set(records.map(r => r.date))].sort();
let streak = 1;
let gaps = 0;
for (let i = 1; i < dates.length; i++) {
const curr = new Date(dates[i]);
const prev = new Date(dates[i - 1]);
const diff = Math.floor((curr - prev) / (1000 * 60 * 60 * 24));
if (diff === 1) {
streak++;
gaps = 0;
} else if (diff <= maxGap + 1) {
// 允许有间隔
streak++;
gaps += diff - 1;
} else {
// 中断
return {
streak: streak,
isActive: false,
breakDate: dates[i]
};
}
}
return { streak, isActive: true, gaps };
}
// 使用示例
const records = [
{ date: '2024-01-10' },
{ date: '2024-01-09' },
{ date: '2024-01-07' }, // 跳过一天
{ date: '2024-01-06' }
];
console.log(calculateEnhancedStreak(records, { maxGap: 1 }));
// 输出: { streak: 4, isActive: true, gaps: 1 }
完整的存储 + 计算方案
class StreakTracker {
constructor(storageKey = 'streak_data') {
this.storageKey = storageKey;
this.data = this.load();
}
// 打卡
checkIn() {
const today = new Date().toISOString().split('T')[0];
if (!this.data.dates.includes(today)) {
this.data.dates.push(today);
this.save();
}
return this.calculateStreak();
}
// 计算连续天数
calculateStreak() {
const dates = [...this.data.dates].sort((a, b) =>
new Date(b) - new Date(a)
);
if (dates.length === 0) return 0;
let streak = 1;
const today = new Date().toISOString().split('T')[0];
// 检查最后打卡日期
const lastDate = dates[0];
const diff = Math.floor(
(new Date(today) - new Date(lastDate)) / (1000 * 60 * 60 * 24)
);
if (diff > 1) return 0; // 中断
// 计算连续
for (let i = 1; i < dates.length; i++) {
const prevDate = dates[i - 1];
const currDate = dates[i];
const diff = Math.floor(
(new Date(prevDate) - new Date(currDate)) / (1000 * 60 * 60 * 24)
);
if (diff === 1) {
streak++;
} else {
break;
}
}
return streak;
}
load() {
const data = localStorage.getItem(this.storageKey);
return data ? JSON.parse(data) : { dates: [] };
}
save() {
localStorage.setItem(this.storageKey, JSON.stringify(this.data));
}
// 获取打卡统计
getStats() {
return {
streak: this.calculateStreak(),
totalCheckins: this.data.dates.length,
lastCheckin: this.data.dates[this.data.dates.length - 1] || null,
dates: this.data.dates
};
}
}
// 使用示例
const tracker = new StreakTracker();
tracker.checkIn(); // 打卡
console.log(tracker.getStats()); // 获取统计
关键考虑因素
- 时间处理:注意时区问题,使用 UTC 或统一时区
- 边界情况:今天未打卡、昨天未打卡等情况
- 数据去重:同一天多次打卡应只算一次
- 性能优化:大量数据时考虑使用缓存
选择哪种实现取决于你的具体场景:前端、后端、还是数据库层面。