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

Flutter+Firestore考勤功能异常:用户字段存入考勤集合为空

问题排查与修复方案

核心问题分析

你的代码里uid、studentname、fathername存为空,主要是这几个原因:

  1. 用户未登录就进入仪表盘:FirebaseAuth.instance.currentUser可能为null,导致uid是空值,后续读取users集合时用了空的文档ID,自然拿不到数据。
  2. Firestore字段名不匹配:代码里用的studentname、fathername,如果你的Firestoreusers集合里实际字段名是驼峰式(比如studentName、fatherName),就会读取失败,最终赋值为空字符串。
  3. 缺少空值拦截与错误处理:没有对uid为空的情况做拦截,也没处理读取用户数据时的异常,导致问题发生时无法快速定位。

修复后的完整代码

import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Attendance App',
      theme: ThemeData(primarySwatch: Colors.blue),
      // 先校验登录状态,未登录跳转到登录页(可替换成你自己的登录组件)
      home: StreamBuilder<User?>(
        stream: FirebaseAuth.instance.authStateChanges(),
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            return const UserDashBoard();
          } else {
            return const Scaffold(body: Center(child: Text('请先登录')));
          }
        },
      ),
    );
  }
}

class UserDashBoard extends StatefulWidget {
  const UserDashBoard({super.key});

  @override
  _UserDashBoardState createState() => _UserDashBoardState();
}

class _UserDashBoardState extends State<UserDashBoard> {
  bool hasMarkedAttendance = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('User Panel')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: hasMarkedAttendance ? null : markAttendance,
              child: const Text('Mark Attendance'),
            ),
          ],
        ),
      ),
    );
  }

  void markAttendance() async {
    // 1. 先确保用户已登录,获取非空的uid
    final User? currentUser = FirebaseAuth.instance.currentUser;
    if (currentUser == null) {
      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: const Text('错误'),
          content: const Text('请先登录再打卡'),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('确定'),
            ),
          ],
        ),
      );
      return;
    }
    final String uid = currentUser.uid;
    String studentname = '';
    String fatherName = '';
    final DateTime currentDate = DateTime.now();

    try {
      // 2. 读取用户数据,增加异常捕获
      final DocumentSnapshot userSnap = await FirebaseFirestore.instance
          .collection('users')
          .doc(uid)
          .get();

      if (userSnap.exists) {
        // 注意:这里的字段名要和Firestore里的完全一致!
        // 如果Firestore里是studentName,就改成userSnap.get('studentName')
        studentname = userSnap.get('studentname') ?? '';
        fatherName = userSnap.get('fathername') ?? '';
      } else {
        showDialog(
          context: context,
          builder: (context) => AlertDialog(
            title: const Text('错误'),
            content: const Text('用户信息不存在,请联系管理员'),
            actions: [
              TextButton(
                onPressed: () => Navigator.pop(context),
                child: const Text('确定'),
              ),
            ],
          ),
        );
        return;
      }
    } catch (e) {
      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: const Text('错误'),
          content: Text('读取用户信息失败:$e'),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('确定'),
            ),
          ],
        ),
      );
      return;
    }

    // 3. 检查今日是否已打卡
    final DateTime startOfCurrentDate = DateTime(currentDate.year, currentDate.month, currentDate.day);
    final DateTime endOfCurrentDate = startOfCurrentDate.add(const Duration(days: 1));

    final QuerySnapshot attendanceSnapshot = await FirebaseFirestore.instance
        .collection('attendance')
        .where('uid', isEqualTo: uid)
        .where('date', isGreaterThanOrEqualTo: startOfCurrentDate)
        .where('date', isLessThan: endOfCurrentDate)
        .get();

    if (attendanceSnapshot.docs.isNotEmpty) {
      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: const Text('错误'),
          content: const Text('今日已打卡,请勿重复操作'),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('确定'),
            ),
          ],
        ),
      );
      return;
    }

    // 4. 写入打卡记录
    try {
      await FirebaseFirestore.instance.collection('attendance').add({
        'uid': uid,
        'studentname': studentname,
        'fatherName': fatherName,
        'date': currentDate,
      });

      setState(() {
        hasMarkedAttendance = true;
      });

      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: const Text('成功'),
          content: const Text('打卡成功'),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('确定'),
            ),
          ],
        ),
      );
    } catch (e) {
      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: const Text('错误'),
          content: Text('打卡失败:$e'),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('确定'),
            ),
          ],
        ),
      );
    }
  }
}

关键修改点说明

  • 登录状态前置校验:用StreamBuilder监听用户登录状态,未登录时直接拦截,从根源避免uid为空的问题。
  • uid非空拦截:在打卡方法开头就检查用户是否登录,为空直接提示并返回,避免无效代码执行。
  • 增加异常捕获:读取用户数据和写入打卡记录时都加了try-catch,能明确报错原因,方便调试。
  • 字段名匹配提示:特意标注Firestore字段名要和代码完全一致,这是新手常踩的坑。
  • 逻辑拆分优化:把每个步骤拆分并增加返回逻辑,避免后续无效操作。

内容的提问来源于stack exchange,提问作者Muhammad Daniyal Khan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 15:57:20