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

如何通过Firestore安全规则强制多文档事务同步更新(无云函数)

Firestore安全规则:转账事务的双向原子验证(无需云函数)

场景是要同步维护accounts(含balance字段)和transfers(存储转账记录)两个集合,必须确保:

  • 创建transfers文档时,必须同时在事务中更新对应转出/转入账户的余额
  • 更新accounts文档的余额时,必须附带对应的transfers创建操作作为依据,不能单独修改账户余额

现有规则已经完成了transfers的创建验证,下面补充accounts的更新规则,实现双向约束:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /transfers/{transferId} {
      allow create: if request.auth != null &&
        // 验证转出账户余额减少金额等于转账金额
        getAfter(/databases/$(database)/documents/accounts/$(request.resource.data.fromAccountId)).data.balance -
        get(/databases/$(database)/documents/accounts/$(request.resource.data.fromAccountId)).data.balance === -request.resource.data.amount &&
        // 验证转入账户余额增加金额等于转账金额
        getAfter(/databases/$(database)/documents/accounts/$(request.resource.data.toAccountId)).data.balance -
        get(/databases/$(database)/documents/accounts/$(request.resource.data.toAccountId)).data.balance === request.resource.data.amount;
    }
    match /accounts/{accountId} {
      allow update: if request.auth != null &&
        // 确保只修改balance字段,避免无关篡改
        request.writeFields.size() == 1 && request.writeFields.has('balance') &&
        (
          // 情况1:当前账户是转账转出方,余额减少
          (
            request.resource.data.balance < resource.data.balance &&
            existsAfter(/databases/$(database)/documents/transfers/$(request.resource.data.transferId)) &&
            getAfter(/databases/$(database)/documents/transfers/$(request.resource.data.transferId)).data.fromAccountId == accountId &&
            getAfter(/databases/$(database)/documents/transfers/$(request.resource.data.transferId)).data.amount == resource.data.balance - request.resource.data.balance
          ) ||
          // 情况2:当前账户是转账转入方,余额增加
          (
            request.resource.data.balance > resource.data.balance &&
            existsAfter(/databases/$(database)/documents/transfers/$(request.resource.data.transferId)) &&
            getAfter(/databases/$(database)/documents/transfers/$(request.resource.data.transferId)).data.toAccountId == accountId &&
            getAfter(/databases/$(database)/documents/transfers/$(request.resource.data.transferId)).data.amount == request.resource.data.balance - resource.data.balance
          )
        );
    }
  }
}

规则逻辑说明:

  1. transfers创建规则优化:调整了余额变化的计算顺序,更直观体现转出账户余额减少值等于转账金额、转入账户余额增加值等于转账金额
  2. accounts更新规则核心:
    • 先限制只能修改balance字段,避免其他字段被随意改动
    • 分两种余额变化场景做验证:
      • 转出场景:余额减少的金额,必须等于事务中同时创建的转账记录金额,且该记录的转出账户ID就是当前账户ID
      • 转入场景:余额增加的金额,必须等于事务中同时创建的转账记录金额,且该记录的转入账户ID就是当前账户ID
    • 通过existsAfter和getAfter验证事务中确实存在对应的转账记录创建操作,确保两个操作是原子绑定的

客户端事务要求:

执行转账时,客户端必须在同一个事务中完成三个操作:

  1. 创建transfers文档,包含fromAccountId、toAccountId、amount字段
  2. 更新转出账户的balance,同时在更新数据中加入transferId字段(对应刚创建的转账记录ID)
  3. 更新转入账户的balance,同时在更新数据中加入transferId字段(对应同一个转账记录ID)

这样就能彻底避免用户绕过事务单独修改任一集合的情况,完全通过安全规则实现约束,无需依赖云函数。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 02:41:07