You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于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,写入当日日期
  2. 上次签到日期为当日:已签到,直接返回现有连胜数,不做累加
  3. 上次签到日期为昨日:连胜有效,当前连胜数+1,更新上次签到日期为当日
  4. 上次签到日期早于昨日:连胜中断,当前连胜数重置为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 13:15:36