如何基于Firebase+React Native(Expo)实现类多邻国连续签到连胜功能
Hot Streak(连续签到连胜)功能全流程实现方案
以下是可直接落地的实现步骤,适配你现有的Expo + React Native + Firebase技术栈,默认你已经完成Firebase SDK初始化、用户认证模块开发。
第一步:Firebase Firestore 结构配置
首先设计存储连胜数据的集合,直接用用户Auth UID作为文档ID,减少复杂查询:
- 集合名:
userStreaks,单文档对应单个用户的连胜数据 - 文档字段:
currentStreak:number类型,存储当前连续签到天数lastCheckinDate:string类型,存储上次签到的本地自然日,格式固定为YYYY-MM-DD,从根源避免时区判断错误longestStreak:number类型,存储历史最高连胜天数,可选,用于后续成就系统checkinHistory:array类型,存储所有签到日期的字符串集合,用于补签、数据校验
配置Firestore安全规则,保证仅用户本人可读写自己的连胜数据,在Firebase控制台的Firestore规则面板粘贴以下内容:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /userStreaks/{userId} { allow read, write: if request.auth != null && request.auth.uid == userId; } } }
第二步:核心签到逻辑实现
核心判断规则非常简单,每次触发签到逻辑时,拿当日日期和数据库存储的上次签到日期做比对:
- 无历史数据(新用户首次签到):连胜天数初始化为1,写入当日日期
- 上次签到日期为当日:已签到,直接返回现有连胜数,不做累加
- 上次签到日期为昨日:连胜有效,当前连胜数+1,更新上次签到日期为当日
- 上次签到日期早于昨日:连胜中断,当前连胜数重置为1,更新上次签到日期为当日
先写两个工具函数处理日期逻辑,无需引入第三方依赖:
// 生成本地时区的YYYY-MM-DD格式日期字符串 const formatLocalDate = (date = new Date()) => { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }; // 计算两个日期的自然日差 const getDaysDiff = (dateStrA, dateStrB) => { const timeA = new Date(dateStrA).getTime(); const timeB = new Date(dateStrB).getTime(); return Math.round((timeB - timeA) / (1000 * 3600 * 24)); };
基于Firebase v9+ 模块化SDK写签到核心方法:
import { getFirestore, doc, getDoc, setDoc, updateDoc } from 'firebase/firestore'; import { getAuth } from 'firebase/auth'; const db = getFirestore(); const auth = getAuth(); const runDailyCheckin = async () => { const currentUser = auth.currentUser; if (!currentUser) throw new Error('用户未登录,无法执行签到'); const today = formatLocalDate(); const streakDocRef = doc(db, 'userStreaks', currentUser.uid); const streakSnapshot = await getDoc(streakDocRef); // 新用户首次签到 if (!streakSnapshot.exists()) { await setDoc(streakDocRef, { currentStreak: 1, lastCheckinDate: today, longestStreak: 1, checkinHistory: [today] }); return { currentStreak: 1, isNewCheckin: true }; } const savedStreakData = streakSnapshot.data(); // 当日已签到 if (savedStreakData.lastCheckinDate === today) { return { currentStreak: savedStreakData.currentStreak, isNewCheckin: false }; } const dayGap = getDaysDiff(savedStreakData.lastCheckinDate, today); let updatedStreak = dayGap === 1 ? savedStreakData.currentStreak + 1 : 1; const updatedLongest = Math.max(updatedStreak, savedStreakData.longestStreak || 0); await updateDoc(streakDocRef, { currentStreak: updatedStreak, lastCheckinDate: today, longestStreak: updatedLongest, checkinHistory: [...(savedStreakData.checkinHistory || []), today] }); return { currentStreak: updatedStreak, isNewCheckin: true }; };
上线前如果要防作弊(比如用户修改本地时间刷连胜),把这段逻辑迁移到Firebase云函数,用服务器时间做日期计算,同时把安全规则改成仅云函数可写入
userStreaks集合即可,前期开发阶段用前端逻辑足够跑通流程。
第三步:仿多邻国顶部提示UI实现
在用户登录后的首页/全局导航栏引入连胜横幅组件,页面加载、APP从后台切回前台时自动触发签到逻辑,拉取最新连胜数。
组件代码如下:
import { View, Text, StyleSheet, AppState } from 'react-native'; import { useState, useEffect } from 'react'; const StreakTopBanner = () => { const [streakCount, setStreakCount] = useState(0); const [showSuccessTip, setShowSuccessTip] = useState(false); const loadStreakStatus = async () => { const checkinResult = await runDailyCheckin(); setStreakCount(checkinResult.currentStreak); if (checkinResult.isNewCheckin) { setShowSuccessTip(true); setTimeout(() => setShowSuccessTip(false), 2000); } }; useEffect(() => { // 页面首次加载时拉取状态 loadStreakStatus(); // 监听APP切回前台事件,避免后台跨天状态不更新 const appStateSub = AppState.addEventListener('change', (state) => { if (state === 'active') loadStreakStatus(); }); return () => appStateSub.remove(); }, []); return ( <View style={styles.bannerWrap}> <Text style={styles.flameIcon}>🔥</Text> <Text style={styles.bannerText}>您已连续签到{streakCount}天!</Text> {showSuccessTip && ( <View style={styles.successToast}> <Text style={styles.toastText}>签到成功,连胜+1 🎉</Text> </View> )} </View> ); }; const styles = StyleSheet.create({ bannerWrap: { flexDirection: 'row', alignItems: 'center', paddingVertical: 8, paddingHorizontal: 16, backgroundColor: '#FFF7E6', borderBottomWidth: 1, borderBottomColor: '#FFE0B2', position: 'relative' }, flameIcon: { fontSize: 20, marginRight: 8 }, bannerText: { fontSize: 14, fontWeight: '600', color: '#E65100' }, successToast: { position: 'absolute', top: 48, left: '50%', transform: [{ translateX: -90 }], backgroundColor: '#FF9800', paddingHorizontal: 16, paddingVertical: 8, borderRadius: 20, zIndex: 999 }, toastText: { color: '#fff', fontWeight: '600', fontSize: 13 } }); export default StreakTopBanner;
把这个组件放在你页面布局的顶部位置即可,样式和多邻国的连胜提示视觉效果一致,签到成功会自动弹出2秒的提示浮层。
第四步:边界情况处理
- 时区问题:禁止直接存储UTC时间戳做日期判断,必须转成本地自然日的
YYYY-MM-DD字符串,避免国内用户夜间签到出现日期判定错误 - 离线场景:可以加本地缓存兜底,无网络时先读本地AsyncStorage存的上次连胜数,网络恢复后再和Firebase数据同步
- 补签需求:如果后续要做补签功能,只需要修改
checkinHistory数组,重新计算连续签到的逻辑即可,现有数据结构完全支持
内容的提问来源于stack exchange,提问作者Corey Sutton
相关产品推荐
相关产品推荐

