Flutter+Firestore考勤功能异常:用户字段存入考勤集合为空
问题排查与修复方案
核心问题分析
你的代码里uid、studentname、fathername存为空,主要是这几个原因:
- 用户未登录就进入仪表盘:
FirebaseAuth.instance.currentUser可能为null,导致uid是空值,后续读取users集合时用了空的文档ID,自然拿不到数据。 - Firestore字段名不匹配:代码里用的
studentname、fathername,如果你的Firestoreusers集合里实际字段名是驼峰式(比如studentName、fatherName),就会读取失败,最终赋值为空字符串。 - 缺少空值拦截与错误处理:没有对
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
相关产品推荐
相关产品推荐

