Flutter Firebase投票应用限制单次投票失效问题求助
Hey there! Let's break down why your one-vote restriction isn't working and fix it step by step—since you're new to Flutter/Dart, I'll keep things clear and actionable.
Key Issues in Your Code
Let's start with the root causes of the problem:
- Wrong Widget structure + misusing
initState: Your_checkHasVotedis defined as a Widget, but you putinitStateinside it.initStateis only meant to run in aStateclass (like your_FinanceResultState), so that check logic never actually executes. - Incorrect document reference:
Firestore.instance.collection("finance").document()creates a brand new empty document reference, not the one the user tapped on. You need to use the reference of the specific voting item the user selected. - Broken vote status check: You're checking if the document data directly contains the user's UID, but your vote status is stored in the
hasVotedmap field. You should check if that map contains the user's UID. - Forgot to update
hasVoted: When a user votes, you only increment thevotescount but don't add their UID tohasVoted—so the app never registers that they've already voted. - No unauthenticated user handling: If a user isn't logged in,
currentUser()returns null, which will break your logic.
Fixed Code Implementation
Let's rewrite the vote-checking logic properly as an async method in your _FinanceResultState class, and fix all the issues:
class _FinanceResultState extends State<FinanceResult> { List<charts.Series<Record, String>> _seriesBarData; List<Record> mydata; _generateData(mydata) { _seriesBarData = List<charts.Series<Record, String>>(); _seriesBarData.add( charts.Series( domainFn: (Record record, _) => record.name.toString(), measureFn: (Record record, _) => record.totalVotes, id: 'Record', data: mydata, labelAccessorFn: (Record row, _) => '${row.name}: ${row.totalVotes}', colorFn: (_, __) => charts.MaterialPalette.cyan.shadeDefault, fillColorFn: (_, __) => charts.MaterialPalette.transparent, ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Finance Result'), leading: IconButton( icon: Icon(Icons.home), onPressed: () { Navigator.push(context, TransitionPageRoute(widget: HomePage())); }, ), ), body: Container( color: Colors.grey.shade100, child: _buildBody(context), )); } Widget _buildBody(BuildContext context) { return Column( children: <Widget>[ StreamBuilder<QuerySnapshot>( stream: Firestore.instance.collection('finance').snapshots(), builder: (context, snapshot) { if (!snapshot.hasData) { return Center( child: LinearProgressIndicator( valueColor: AlwaysStoppedAnimation( Theme.of(context).primaryColor, ), ), ); } else { List<Record> finance = snapshot.data.documents .map((documentSnapshot) => Record.fromMap(documentSnapshot.data)) .toList(); return _buildChart(context, finance); } }, ), ], ); } Widget _buildChart(BuildContext context, List<Record> recorddata) { mydata = recorddata; _generateData(mydata); return Padding( padding: EdgeInsets.all(8.0), child: Container( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height / 1.2, child: Center( child: Column( children: <Widget>[ SizedBox(height: 10.0), Expanded( child: charts.PieChart( _seriesBarData, animate: true, animationDuration: Duration(seconds: 3), defaultRenderer: new charts.ArcRendererConfig( strokeWidthPx: 2.0, arcWidth: 100, arcRendererDecorators: [ charts.ArcLabelDecorator( labelPosition: charts.ArcLabelPosition.auto, labelPadding: 3, showLeaderLines: true, insideLabelStyleSpec: charts.TextStyleSpec( color: charts.Color.white, fontSize: 12, ), outsideLabelStyleSpec: charts.TextStyleSpec( color: charts.Color.black, fontSize: 12, ), ), ]), ), ), Container( width: MediaQuery.of(context).size.width / 3.5, height: 1, color: Colors.black38, ), Expanded( child: Scaffold( backgroundColor: Colors.grey.shade100, body: _castVote(context), ), ), ], ), ), ), ); } Widget _castVote(BuildContext context) { return StreamBuilder<QuerySnapshot>( stream: Firestore.instance.collection('finance').snapshots(), builder: (context, snapshot) { if (!snapshot.hasData) return Center( child: LinearProgressIndicator( valueColor: AlwaysStoppedAnimation( Theme.of(context).primaryColor, ), ), ); return _buildList(context, snapshot.data.documents); }, ); } Widget _buildList(BuildContext context, List<DocumentSnapshot> snapshot) { return ListView( padding: const EdgeInsets.only(top: 20.0), children: snapshot.map((data) => _buildListItem(context, data)).toList(), ); } Widget _buildListItem(BuildContext context, DocumentSnapshot data) { final record = Record.fromSnapshot(data); return Padding( key: ValueKey(record.name), padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Card( elevation: 2, child: Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade100), borderRadius: BorderRadius.circular(5.0), ), child: ListTile( title: Text(record.name), trailing: Text(record.totalVotes.toString()), onTap: () => _checkAndCastVote(context, data), // Call our fixed method ), ), ), ); } /// Fixed vote checking & casting logic Future<void> _checkAndCastVote(BuildContext context, DocumentSnapshot data) async { // Get current logged-in user first final user = await FirebaseAuth.instance.currentUser(); if (user == null) { Scaffold.of(context).showSnackBar( SnackBar(content: Text("Please log in to vote!")), ); return; } final record = Record.fromSnapshot(data); final docRef = record.reference; // Fetch the latest data from Firestore to avoid stale reads final snapshot = await docRef.get(); final itemData = snapshot.data; // Check if hasVoted exists, or default to empty map final hasVotedMap = itemData['hasVoted'] as Map<String, bool> ?? {}; // Check if user already voted if (hasVotedMap.containsKey(user.uid)) { Scaffold.of(context).showSnackBar( SnackBar(content: Text("Sorry you have voted in this category already")), ); return; } // Update vote count AND mark user as voted await docRef.updateData({ 'votes': record.totalVotes + 1, 'hasVoted': Map<String, bool>.from(hasVotedMap)..[user.uid] = true, }); Scaffold.of(context).showSnackBar( SnackBar(content: Text("Vote cast successfully!")), ); } }
Critical Extra Step: Firestore Security Rules
Client-side checks can be bypassed, so you must add Firestore security rules to enforce the one-vote restriction on the backend. Go to your Firestore console, navigate to Rules, and replace the default rules with this:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /finance/{financeDoc} { allow read: if true; // Allow anyone to view results allow update: if request.auth != null && !resource.data.hasVoted.keys().hasAny([request.auth.uid]) && request.resource.data.votes == resource.data.votes + 1 && request.resource.data.hasVoted.keys().hasAny([request.auth.uid]); } } }
This rule ensures:
- Only logged-in users can vote
- Users can only vote once per item
- Vote counts can only increment by 1
- Users must add their UID to
hasVotedwhen voting
Bonus Tip
You're listening to the finance collection twice (once in _buildBody, once in _castVote). To optimize performance, you could merge these into a single StreamBuilder that feeds data to both the chart and the vote list.
内容的提问来源于stack exchange,提问作者Kennedy Owusu

