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

如何在Flutter Web项目中实现Supabase自定义受访者认证

解决方案:自定义受访者登录逻辑实现

1. 修正认证查询与错误处理

你的核心问题在于使用.single()时,查询无结果会直接抛出异常而非返回空值,需用try-catch捕获异常,同时补充PIN过期校验逻辑:

修改login_page.dart中的按钮点击事件:

ElevatedButton(
  onPressed: () async {
    final supabase = Supabase.instance.client;
    final householdNumber = _householdNumberController.text.trim();
    final pin = _pinPasswordController.text.trim();

    // 输入合法性校验
    if (householdNumber.isEmpty || pin.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('请填写完整信息')),
      );
      return;
    }

    try {
      // 查询匹配的受访者,同时获取过期时间用于校验
      final response = await supabase
          .from('respondents')
          .select('id, household_number, lastname, pin_expiry, barangay_id, purok_id')
          .eq('household_number', householdNumber)
          .eq('pin_password', pin)
          .single();

      // 校验PIN是否过期
      final expiryDate = DateTime.parse(response['pin_expiry']);
      if (DateTime.now().isAfter(expiryDate)) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('PIN码已过期')),
        );
        return;
      }

      // 认证通过,跳转至调研页面
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('登录成功')),
      );
      Navigator.pushReplacement(
        context,
        MaterialPageRoute(
          builder: (context) => SurveyPage(respondent: response),
        ),
      );
    } on PostgrestException catch (e) {
      // 处理Supabase查询异常,无匹配结果返回406码
      if (e.code == '406') {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('家庭编号或PIN码错误')),
        );
      } else {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('登录失败:${e.message}')),
        );
      }
    } catch (e) {
      // 捕获其他未知错误
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('发生未知错误:$e')),
      );
    }
  },
  child: const Text('Login'),
),

2. 创建Respondent数据模型类

创建模型类实现类型安全的数据操作,避免直接操作Map:

新建models/respondent.dart:

class Respondent {
  final String id;
  final String householdNumber;
  final String lastName;
  final DateTime pinExpiry;
  final int barangayId;
  final int purokId;

  Respondent({
    required this.id,
    required this.householdNumber,
    required this.lastName,
    required this.pinExpiry,
    required this.barangayId,
    required this.purokId,
  });

  // 从Supabase返回的Map转换为模型对象
  factory Respondent.fromMap(Map<String, dynamic> map) {
    return Respondent(
      id: map['id'],
      householdNumber: map['household_number'],
      lastName: map['lastname'],
      pinExpiry: DateTime.parse(map['pin_expiry']),
      barangayId: map['barangay_id'],
      purokId: map['purok_id'],
    );
  }

  // 转换为Map,方便存储或上传
  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'household_number': householdNumber,
      'lastname': lastName,
      'pin_expiry': pinExpiry.toIso8601String(),
      'barangay_id': barangayId,
      'purok_id': purokId,
    };
  }
}

登录成功时将查询结果转为模型对象:

final respondent = Respondent.fromMap(response);
Navigator.pushReplacement(
  context,
  MaterialPageRoute(
    builder: (context) => SurveyPage(respondent: respondent),
  ),
);

3. 保留会话/用户详情

不使用Supabase标准认证时,可通过以下两种方式保留用户会话:

方式一:本地存储(SharedPreferences)

添加shared_preferences依赖到pubspec.yaml,登录成功后存储用户信息:

import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

// 登录成功后存储
final prefs = await SharedPreferences.getInstance();
await prefs.setString('respondent', jsonEncode(respondent.toMap()));

App启动时检查本地存储,直接跳转调研页面:
修改main.dart中的WebSurvey组件:

home: FutureBuilder(
  future: checkSavedRespondent(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const CircularProgressIndicator();
    }
    return snapshot.hasData ? SurveyPage(respondent: snapshot.data!) : const LoginPage();
  },
),

添加检查方法:

Future<Respondent?> checkSavedRespondent() async {
  final prefs = await SharedPreferences.getInstance();
  final respondentJson = prefs.getString('respondent');
  if (respondentJson != null) {
    final map = jsonDecode(respondentJson);
    return Respondent.fromMap(map);
  }
  return null;
}

方式二:全局状态管理(如Provider)

若需全局访问用户信息,推荐使用Provider或Riverpod管理状态,避免页面间频繁传递数据。

4. 调研页面示例

创建SurveyPage接收Respondent对象:

import 'package:flutter/material.dart';
import '../models/respondent.dart';

class SurveyPage extends StatelessWidget {
  final Respondent respondent;

  const SurveyPage({super.key, required this.respondent});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('调研页面')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('欢迎,${respondent.lastName}先生/女士'),
            Text('家庭编号:${respondent.householdNumber}'),
            // 此处添加调研内容组件
          ],
        ),
      ),
    );
  }
}

注意事项

  • 开放Supabase表查询权限:在Supabase控制台SQL编辑器执行以下语句,允许匿名用户查询受访者表:
ALTER POLICY "Allow anon to select respondents" ON public.respondents
FOR SELECT USING (true);
  • PIN码加密存储:当前PIN为明文存储,存在安全风险,建议插入时用crypt函数加密:
-- 插入加密PIN
INSERT INTO respondents (household_number, pin_password, ...)
VALUES ('1234', crypt('123456', gen_salt('bf')), ...);

-- 查询时匹配加密PIN
SELECT * FROM respondents 
WHERE household_number = '1234' 
AND pin_password = crypt('123456', pin_password);

内容的提问来源于stack exchange,提问作者Sheenergizer

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 12:43:17