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

Flutter中Firestore付费用户云同步、免费用户离线缓存及同步优化咨询

针对Firestore免费/付费用户同步的解决方案

你的核心问题是免费用户转付费时,本地积累的离线操作会批量同步产生大量事务,这既可能触发Firestore的配额限制,也会浪费资源。以下是几个纯Firestore的可行方案,按推荐优先级排序:

一、路径隔离:免费/付费数据分存

这是最稳妥的方案,通过将免费用户的数据完全隔离在一个不允许云端同步的路径下,从根源避免离线操作进入同步队列。

实现思路

  1. 数据路径划分:
    • 免费用户:数据存在free-users/{userId}/lists路径
    • 付费用户:数据存在users/{userId}/lists路径
  2. 安全规则限制:给free-users路径设置禁止云端写入的规则,确保免费期间的所有操作仅停留在本地缓存,不会上传到云端。
  3. 转付费迁移:用户订阅激活后,主动将free-users路径下的本地缓存数据批量迁移到users路径,之后切换到付费路径进行正常云端同步。

代码调整示例

1. 动态切换集合引用

// 新增:根据订阅状态返回对应的集合引用
CollectionReference get _currentListsRef {
  final userId = FirebaseAuth.instance.currentUser!.uid;
  return subscriptionProvider.hasActiveSubscription
      ? _firestore.collection('users').doc(userId).collection('lists')
      : _firestore.collection('free-users').doc(userId).collection('lists');
}

// 修改构造函数和监听逻辑
ListProvider(this.subscriptionProvider) {
  _initializeDataFlow();
}

// 新增:保存订阅监听对象,避免内存泄漏
StreamSubscription? _listsSubscription;

void _listenToLists() {
  _isLoading = true;
  notifyListeners();
  // 取消之前的监听
  _listsSubscription?.cancel();
  // 监听当前路径的快照
  _listsSubscription = _currentListsRef.orderBy('orderField').snapshots().listen((snapshot) {
    _lists = snapshot.docs.map((doc) => ListModel.fromMap(doc.data() as Map<String, dynamic>)).toList();
    _isLoading = false;
    notifyListeners();
  });
}

2. 转付费数据迁移逻辑

// 新增:记录之前的订阅状态,用于判断是否需要迁移
bool _hasActiveSubscription = false;

Future<void> _handleSubscriptionChange() async {
  final wasSubscribed = _hasActiveSubscription;
  _hasActiveSubscription = subscriptionProvider.hasActiveSubscription;

  if (_hasActiveSubscription) {
    await _firestore.enableNetwork();
    print("Network enabled: Syncing with Firestore.");
    // 从免费转付费时执行数据迁移
    if (!wasSubscribed) {
      await _migrateFreeUserData();
    }
  } else {
    await _firestore.disableNetwork();
    print("Network disabled: Using local cache only.");
  }
  _listenToLists();
}

Future<void> _migrateFreeUserData() async {
  final userId = FirebaseAuth.instance.currentUser!.uid;
  final freeListsRef = _firestore.collection('free-users').doc(userId).collection('lists');
  final paidListsRef = _firestore.collection('users').doc(userId).collection('lists');

  // 仅读取本地缓存的免费数据
  final snapshot = await freeListsRef.get(const GetOptions(source: Source.cache));
  if (snapshot.docs.isEmpty) return;

  // 批量写入付费路径(Firestore批量操作最多支持500个操作)
  WriteBatch batch = _firestore.batch();
  int batchCount = 0;
  for (final doc in snapshot.docs) {
    batch.set(paidListsRef.doc(doc.id), doc.data());
    batchCount++;
    if (batchCount % 500 == 0) {
      await batch.commit();
      batch = _firestore.batch();
    }
  }
  // 提交剩余的操作
  if (batchCount % 500 != 0) {
    await batch.commit();
  }

  // 可选:清理免费路径的本地缓存,避免占用空间
  for (final doc in snapshot.docs) {
    await freeListsRef.doc(doc.id).delete(const DeleteOptions(source: Source.cache));
  }
}

3. Firestore安全规则配置

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // 免费用户路径:仅允许用户自己读取,禁止任何云端写入
    match /free-users/{userId}/{documents=**} {
      allow read: if request.auth.uid == userId;
      allow write: if false;
    }
    // 付费用户路径:仅允许订阅用户读写(需通过自定义声明标记订阅状态)
    match /users/{userId}/{documents=**} {
      allow read, write: if request.auth.uid == userId && request.auth.token.isSubscriber == true;
    }
  }
}

二、字段标记:用安全规则过滤同步

如果不想维护两个数据路径,可以给免费用户的文档添加标记字段,通过安全规则阻止其同步到云端,转付费后再批量移除标记。

实现思路

  1. 字段标记:免费用户创建/更新文档时,添加isFreeTier: true字段。
  2. 安全规则控制:仅允许不带isFreeTier字段的文档写入云端,免费用户的写入会被云端拒绝,但本地缓存仍会保留。
  3. 转付费处理:用户订阅后,批量更新本地所有带isFreeTier字段的文档,移除该字段,此时安全规则允许写入,完成同步。

关键代码调整

1. 写入时添加标记

// 创建列表时,免费用户添加标记
Future<void> createList(ListModel list) async {
  final data = list.toMap();
  if (!subscriptionProvider.hasActiveSubscription) {
    data['isFreeTier'] = true;
  }
  await _currentListsRef.add(data);
}

2. 转付费批量更新

Future<void> _migrateFreeUserData() async {
  final userId = FirebaseAuth.instance.currentUser!.uid;
  final listsRef = _firestore.collection('users').doc(userId).collection('lists');

  // 获取本地缓存中带isFreeTier标记的文档
  final snapshot = await listsRef.where('isFreeTier', isEqualTo: true).get(const GetOptions(source: Source.cache));
  if (snapshot.docs.isEmpty) return;

  WriteBatch batch = _firestore.batch();
  int count = 0;
  for (final doc in snapshot.docs) {
    batch.update(doc.reference, {'isFreeTier': FieldValue.delete()});
    count++;
    if (count % 500 == 0) {
      await batch.commit();
      batch = _firestore.batch();
    }
  }
  if (count % 500 != 0) {
    await batch.commit();
  }
}

3. 安全规则配置

match /users/{userId}/lists/{listId} {
  allow read: if request.auth.uid == userId;
  // 本地缓存写入允许免费用户添加标记
  allow write: if request.auth.uid == userId && (request.source == 'cache' && request.resource.data.isFreeTier == true);
  // 云端写入仅允许订阅用户提交无标记的文档
  allow write: if request.auth.uid == userId && request.auth.token.isSubscriber == true && !('isFreeTier' in request.resource.data);
}

方案对比

方案优点缺点
路径隔离完全避免免费数据进云端,迁移可控需要维护两个数据路径
字段标记路径统一,逻辑简单免费期间写入会触发云端拒绝(不影响本地缓存)

注意事项

  1. 订阅状态验证:务必通过Firebase Auth自定义声明或安全规则验证订阅状态,避免客户端篡改。
  2. 批量操作限制:Firestore批量操作最多支持500个操作,迁移时需分批处理。
  3. 本地缓存清理:转付费后清理免费路径的本地缓存,避免占用设备存储空间。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 08:51:04