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

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 _checkHasVoted is defined as a Widget, but you put initState inside it. initState is only meant to run in a State class (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 hasVoted map field. You should check if that map contains the user's UID.
  • Forgot to update hasVoted: When a user votes, you only increment the votes count but don't add their UID to hasVoted—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 hasVoted when 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.07 20:02:38