15 · 权限与设备能力
⚠️ 本章最重要的一件事:项目里有两套完全无关的「权限」,只是恰好放在同一个目录。 混淆它们是新手最常见的错误。
开篇:两套权限的区分
| 业务权限 | 系统权限 | |
|---|---|---|
| 问的问题 | 「这个用户能用这个功能吗」 | 「这个 App 能用相机吗」 |
| 数据源 | 后端下发的权限码集合 | 操作系统 |
| 本项目的正确入口 | hasPermission('app:b:xxx') | PermissionGuard.ensureGranted(...) |
| 所在文件 | lib/ui/auth/models/auth_state.dart | lib/core/permission/permission_guard.dart |
| 前端类比 | 后台 RBAC 菜单/按钮权限 | navigator.geolocation 类浏览器权限 |
文件位置提示(permission_service.dart:4-6 的注释原文):
与同目录的设备权限文件(
permission_guard.dart/permission_status.dart/permission_rationale.dart/permission_platform.dart,处理 location/camera/photos/notification)无关——二者只是共存于lib/core/permission/目录。
第一部分:业务权限(RBAC)
一、用法(记住这一种)
final canAddPlant =
ref.watch(authViewModelProvider).hasPermission('app:b:addPlant');
return Column(
children: [
if (canAddPlant) HeButton(text: l10n.t('add'), onPressed: _onAdd),
],
);或置灰 + 提示的风格:
void _onAdd(BuildContext context, RuntimeI18n l10n, bool hasPermission) {
if (!hasPermission) {
HeLoading.showToast(l10n.t('noPermissionShort'));
return;
}
context.push(Routes.plantAdd);
}项目实例 —— lib/ui/home/views/agent_home_page.dart:59-60:
final canAddPlant =
ref.watch(authViewModelProvider).hasPermission('app:b:addPlant');
final canDeletePlant =
ref.watch(authViewModelProvider).hasPermission('app:b:deletePlant');二、底层实现
hasPermission 是 extension 方法
⚠️ 它不在 AuthState 类里,而是外挂的 extension —— 因为 AuthState 是 freezed 生成的类,不能直接改。
lib/ui/auth/models/auth_state.dart:145-174:
extension AuthStateX on AuthState {
bool get isAuthenticated =>
this is LoginAuthenticated || this is CompleteLoginAuthenticated;
/// 权限码集合(未登录/不支持时为 null)
List<String>? get permissions { ... }
/// 权限判断:超管通配 `*:*:*` 或精确匹配
bool hasPermission(String code) {
final perms = permissions;
if (perms == null || perms.isEmpty) return false;
return perms.any((p) => p == '*:*:*' || p == code);
}
}两个关键点
① 超管通配
perms.any((p) => p == '*:*:*' || p == code)后端下发 *:*:* 表示该用户是超管,所有权限通过。
② 权限码为空时返回 false
if (perms == null || perms.isEmpty) return false;「没有权限数据」= 「没有权限」(fail-closed),这是安全设计。
真实权限码
从代码中找到的:
| 权限码 | 含义 |
|---|---|
app:b:addPlant | B 端新增电站 |
app:b:deletePlant | B 端删除电站 |
app:b:addDevice | B 端新增设备 |
app:b:delivery | B 端电站移交 |
命名规律:app:b:xxx(B 端安装商)/ app:c:xxx(C 端用户)。
三、⚠️ 废弃链路:PermissionService / AppFeature
这是什么
lib/core/permission/permission_service.dart 定义了一套基于 UserInfo.menus 的菜单权限:
enum AppFeature {
/// 首页(menuKey `app.subcenter.home`;routePath 取 `Routes.home`=`/`)
home('app.subcenter.home', '/');
const AppFeature(this.menuKey, this.routePath);
final String menuKey;
final String routePath;
}
class PermissionService {
bool hasPermission(AppFeature feature) => _menuKeys.contains(feature.menuKey);
}注意它和 AuthStateX.hasPermission 同名但完全不同——参数类型不同(枚举 vs 字符串)。
为什么废弃
证据确凿:全项目 PermissionService / AppFeature 只出现在 5 个文件:
| 文件 | 出现次数 | 性质 |
|---|---|---|
lib/core/permission/permission_service.dart | 10 处 | 定义自身 |
lib/config/providers/permission_service_provider.dart | 17 处 | DI 注册 |
lib/config/dependencies.dart | 3 处 | 注入 router |
lib/core/permission/permission_bridge.dart | 2 处 | 桥接 |
lib/routing/router.dart | 1 处 | 守卫里取用 |
业务页面零使用。这是典型的「已废弃但 DI 还挂着」的中间状态。
而且 AppFeature 枚举只剩一个值 home——说明这套方案基本没推广开。
你的行动准则
// ❌ 不要用,不要新增,不要扩展
ref.read(permissionServiceProvider).hasPermission(AppFeature.home)
userInfo.menus // ❌ 废弃字段
// ✅ 只用这个
ref.watch(authViewModelProvider).hasPermission('app:b:xxx')理由(AGENTS.md 明文):PermissionService / AppFeature / UserInfo.menus 链路已废弃,勿再扩展。
第二部分:系统权限(设备能力)
四、PermissionGuard
定义于 lib/core/permission/permission_guard.dart,封装了完整的权限请求流程。
支持的权限
AppPermission 枚举共 4 个值:
enum AppPermission { location, camera, photos, notification }| 值 | 用途 |
|---|---|
location | 定位(添加电站时定位地址) |
camera | 相机(扫码、拍照) |
photos | 相册(选图上传) |
notification | 通知(告警推送) |
API
lib/core/permission/permission_guard.dart:20-58:
class PermissionGuard {
PermissionGuard({required PermissionPlatform platform, required GlobalKey<NavigatorState> navigatorKey});
/// 按权限取说明文案模型
PermissionRationale rationaleFor(AppPermission permission, RuntimeI18n l10n);
/// 请求单个权限,返回是否授权
Future<bool> ensureGranted(AppPermission permission);
/// 批量请求多个权限,返回是否全部授权
Future<bool> ensureAllGranted(List<AppPermission> permissions);
}使用
// 单个
final granted = await ref
.read(permissionGuardProvider)
.ensureGranted(AppPermission.camera);
if (!granted || !mounted) return; // ⚠️ 双重检查
setState(() { ... });
// 批量
final allGranted = await ref
.read(permissionGuardProvider)
.ensureAllGranted([AppPermission.camera, AppPermission.photos]);项目实例 —— lib/ui/family/views/family_join_page.dart:34-37:
final granted = await ref
.read(permissionGuardProvider)
.ensureGranted(AppPermission.camera);
if (!granted || !mounted) return;
setState(() { ... });⚠️ !granted || !mounted 是标准写法——请求权限是异步的,期间用户可能退出页面(呼应 03 章)。
Provider 注册
lib/config/providers/platform_providers.dart:74-80:
@riverpod
PermissionGuard permissionGuard(Ref ref) {
return PermissionGuard(
platform: ref.watch(permissionPlatformProvider),
navigatorKey: appNavigatorKey,
);
}五、权限状态
lib/core/permission/permission_status.dart:
enum PermissionStatus { granted, denied, permanentlyDenied, restricted, undetermined }语义
| 值 | 含义 | 处理 |
|---|---|---|
granted | 已授权 | 正常流程 |
denied | 被拒绝(还可以再弹) | 可再次请求 |
permanentlyDenied | 永久拒绝(系统不再弹窗) | 引导去系统设置 |
restricted | 受限制(如家长控制) | 提示无法使用 |
undetermined | 未决定(首次) | 正常请求 |
extension 便捷方法
permission_status.dart:19-25:
extension PermissionStatusExt on PermissionStatus {
bool get isGranted => this == PermissionStatus.granted;
bool get isPermanentlyDenied => this == PermissionStatus.permanentlyDenied;
bool get isDenied =>
this == PermissionStatus.denied || isPermanentlyDenied;
}⚠️ 关键区分:denied vs permanentlyDenied
首次请求 → 系统弹窗 → 用户拒绝 → denied
再次请求 → 系统弹窗 → 用户拒绝 → denied
拒绝时勾选「不再询问」→ 之后请求 → 系统**不再弹窗** → permanentlyDeniedpermanentlyDenied 时必须引导用户去系统设置——PermissionGuard 内部已处理(弹窗引导打开设置)。
六、WebView 中的权限桥接
项目有 WebView(lib/core/webview/),H5 页面可能需要请求系统权限。
场景:H5 调 getUserMedia() 需要相机权限。
处理方式:在 WebView 的 onPermissionRequest 回调里桥接到 PermissionGuard:
onPermissionRequest: (request) async {
// 把 WebView 的权限类型映射成 AppPermission
// 用 PermissionGuard.ensureGranted 请求
// 把结果回传给 WebView
}详见 lib/core/webview/ 下的实现。
七、其他设备能力
lib/core/ 下还有:
| 目录 | 能力 |
|---|---|
lib/core/platform/(13 个) | 平台能力抽象与实现(权限、设备信息) |
lib/core/webview/ | WebView 组件 + JS Bridge |
lib/core/pdf/ | PDF 预览页 |
定位
添加电站时需要定位,走 PermissionGuard.ensureGranted(AppPermission.location),拿到权限后再调定位 SDK。
八、权限检查清单
写涉及权限的代码时自查:
业务权限
- [ ] 用
ref.watch(authViewModelProvider).hasPermission('app:b:xxx') - [ ] 没有用
PermissionService/AppFeature/UserInfo.menus - [ ] 无权限时给了明确反馈(隐藏 或 置灰+提示)
- [ ] 权限码拼写与后端一致
系统权限
- [ ] 用
PermissionGuard.ensureGranted(AppPermission.xxx) - [ ] 异步后检查
!granted || !mounted - [ ] 理解
permanentlyDenied需要引导去设置(Guard 已处理) - [ ] Android 的
AndroidManifest.xml和 iOS 的Info.plist已声明对应权限
九、自检清单
- [ ] 能说出业务权限与系统权限的区别与各自入口
- [ ] 知道
hasPermission是AuthState的 extension(因为 AuthState 是 freezed 类) - [ ] 知道
*:*:*是超管通配 - [ ] 知道权限集合为空时返回
false(fail-closed) - [ ] 知道
PermissionService/AppFeature已废弃,不要用 - [ ] 知道
AppPermission有 4 个值 - [ ] 知道
denied与permanentlyDenied的区别 - [ ] 异步请求权限后会检查
mounted