Skip to content

15 · 权限与设备能力

⚠️ 本章最重要的一件事:项目里有两套完全无关的「权限」,只是恰好放在同一个目录。 混淆它们是新手最常见的错误。


开篇:两套权限的区分

业务权限系统权限
问的问题「这个用户能用这个功能吗」「这个 App 能用相机吗」
数据源后端下发的权限码集合操作系统
本项目的正确入口hasPermission('app:b:xxx')PermissionGuard.ensureGranted(...)
所在文件lib/ui/auth/models/auth_state.dartlib/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)

一、用法(记住这一种)

dart
final canAddPlant =
    ref.watch(authViewModelProvider).hasPermission('app:b:addPlant');

return Column(
  children: [
    if (canAddPlant) HeButton(text: l10n.t('add'), onPressed: _onAdd),
  ],
);

或置灰 + 提示的风格:

dart
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

dart
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

dart
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);
  }
}

两个关键点

① 超管通配

dart
perms.any((p) => p == '*:*:*' || p == code)

后端下发 *:*:* 表示该用户是超管,所有权限通过

② 权限码为空时返回 false

dart
if (perms == null || perms.isEmpty) return false;

「没有权限数据」= 「没有权限」(fail-closed),这是安全设计。

真实权限码

从代码中找到的:

权限码含义
app:b:addPlantB 端新增电站
app:b:deletePlantB 端删除电站
app:b:addDeviceB 端新增设备
app:b:deliveryB 端电站移交

命名规律:app:b:xxx(B 端安装商)/ app:c:xxx(C 端用户)。


三、⚠️ 废弃链路:PermissionService / AppFeature

这是什么

lib/core/permission/permission_service.dart 定义了一套基于 UserInfo.menus 的菜单权限

dart
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.dart10 处定义自身
lib/config/providers/permission_service_provider.dart17 处DI 注册
lib/config/dependencies.dart3 处注入 router
lib/core/permission/permission_bridge.dart2 处桥接
lib/routing/router.dart1 处守卫里取用

业务页面零使用。这是典型的「已废弃但 DI 还挂着」的中间状态。

而且 AppFeature 枚举只剩一个值 home——说明这套方案基本没推广开。

你的行动准则

dart
// ❌ 不要用,不要新增,不要扩展
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 个值

dart
enum AppPermission { location, camera, photos, notification }
用途
location定位(添加电站时定位地址)
camera相机(扫码、拍照)
photos相册(选图上传)
notification通知(告警推送)

API

lib/core/permission/permission_guard.dart:20-58

dart
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);
}

使用

dart
// 单个
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

dart
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

dart
@riverpod
PermissionGuard permissionGuard(Ref ref) {
  return PermissionGuard(
    platform: ref.watch(permissionPlatformProvider),
    navigatorKey: appNavigatorKey,
  );
}

五、权限状态

lib/core/permission/permission_status.dart

dart
enum PermissionStatus { granted, denied, permanentlyDenied, restricted, undetermined }

语义

含义处理
granted已授权正常流程
denied被拒绝(还可以再弹可再次请求
permanentlyDenied永久拒绝(系统不再弹窗)引导去系统设置
restricted受限制(如家长控制)提示无法使用
undetermined未决定(首次)正常请求

extension 便捷方法

permission_status.dart:19-25

dart
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
拒绝时勾选「不再询问」→ 之后请求 → 系统**不再弹窗** → permanentlyDenied

permanentlyDenied 时必须引导用户去系统设置——PermissionGuard 内部已处理(弹窗引导打开设置)。


六、WebView 中的权限桥接

项目有 WebView(lib/core/webview/),H5 页面可能需要请求系统权限。

场景:H5 调 getUserMedia() 需要相机权限。

处理方式:在 WebView 的 onPermissionRequest 回调里桥接到 PermissionGuard

dart
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 已声明对应权限

九、自检清单

  • [ ] 能说出业务权限与系统权限的区别与各自入口
  • [ ] 知道 hasPermissionAuthState 的 extension(因为 AuthState 是 freezed 类)
  • [ ] 知道 *:*:* 是超管通配
  • [ ] 知道权限集合为空时返回 false(fail-closed)
  • [ ] 知道 PermissionService / AppFeature 已废弃,不要用
  • [ ] 知道 AppPermission 有 4 个值
  • [ ] 知道 deniedpermanentlyDenied 的区别
  • [ ] 异步请求权限后会检查 mounted

下一步

16 · 工程化与调试