设置Cloud Firestore规则:允许员工操作对应管理员的店铺资源
多店铺Flutter ERP的Firestore权限与集合结构优化方案
一、集合结构设计:独立集合更合理
你现在把Category和InventoryCat放在管理员文档的子集合里,会导致员工用自己账号登录时根本访问不到——因为子集合绑定在管理员的文档路径下。必须改成独立集合,给每个Category/InventoryCat文档加一个adminId字段(关联对应管理员的用户ID),这样同店铺的管理员和员工都能通过这个字段筛选出属于自己店铺的数据。
这种设计的好处:
- 管理员和员工的查询逻辑统一,不用区分身份
- 数据结构更清晰,便于后续扩展(比如加店铺统计、跨店铺操作等)
- 权限规则更容易编写和维护
二、Firestore规则配置
要实现「仅管理员本人或其staff数组中的员工能读写对应店铺的Category/InventoryCat」,规则可以这么写:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // 判断用户已登录 function isAuthenticated() { return request.auth != null; } // 判断当前用户是目标管理员 function isAdmin(adminId) { return request.auth.uid == adminId; } // 判断当前用户是管理员的员工 function isStaffOfAdmin(adminId) { return exists(/databases/$(database)/documents/users/$(adminId)) && request.auth.uid in get(/databases/$(database)/documents/users/$(adminId)).data.staff; } // Category集合读写权限 match /Category/{categoryId} { allow read, write: if isAuthenticated() && (isAdmin(resource.data.adminId) || isStaffOfAdmin(resource.data.adminId)); } // InventoryCat集合读写权限 match /InventoryCat/{inventoryCatId} { allow read, write: if isAuthenticated() && (isAdmin(resource.data.adminId) || isStaffOfAdmin(resource.data.adminId)); } // 用户集合权限:仅本人可读写自己的文档,员工可读取所属管理员的文档(用于权限校验) match /users/{userId} { allow read, write: if isAuthenticated() && request.auth.uid == userId; allow read: if isAuthenticated() && exists(/databases/$(database)/documents/users/$(userId)) && request.auth.uid in get(/databases/$(database)/documents/users/$(userId)).data.staff; } } }
规则说明:
- 先确保用户已登录,再判断是管理员本人,或是管理员staff数组中的员工,满足任一条件即可读写对应店铺的业务集合
- 用户集合的权限做了隔离,避免越权访问其他用户数据
三、Flutter Stream代码调整
原来的Stream是读取当前用户文档下的子集合,现在改成查询独立集合,需要先获取当前用户对应的adminId(员工的用户文档里要加adminId字段关联所属管理员;管理员的adminId就是自己的uid)。
调整后的代码示例:
// 先获取当前用户对应的adminId Future<String> getCurrentAdminId() async { var userDoc = await _services.userRef.doc(_services.getUserID()).get(); // 管理员返回自己的uid,员工返回所属管理员的uid return userDoc.data()?['adminId'] ?? _services.getUserID(); } // 用FutureBuilder嵌套获取Stream FutureBuilder<String>( future: getCurrentAdminId(), builder: (context, adminIdSnapshot) { if (!adminIdSnapshot.hasData) { return CircularProgressIndicator(); } String adminId = adminIdSnapshot.data!; return StreamBuilder<QuerySnapshot>( stream: _services.categoryRef // 独立集合的引用 .where('adminId', isEqualTo: adminId) .orderBy('name') .snapshots(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return CircularProgressIndicator(); } if (snapshot.hasError) { return Text('加载失败: ${snapshot.error}'); } // 原有UI构建逻辑不变 return ListView.builder( itemCount: snapshot.data?.docs.length ?? 0, itemBuilder: (context, index) { // 处理文档数据 var doc = snapshot.data!.docs[index]; return ListTile(title: Text(doc['name'])); }, ); }, ); }, )
补充注意点
- 管理员添加员工时,除了把员工uid加入自己的
staff数组,还要给员工的用户文档设置adminId字段为自己的uid - 不需要让员工用管理员账号登录,员工用自己账号就能访问对应店铺数据,更符合权限隔离原则
内容的提问来源于stack exchange,提问作者Giovanni
相关产品推荐
相关产品推荐

