Files
ttstd_family_care/AGENTS.md
TongTongStudio 06cdc77172 feat(android): 支持沉浸式状态栏、地图定位与自定义签名
- 配置 edge-to-edge 沉浸式状态栏与刘海屏适配
- 新增百度地图权限、AK 配置及定位权限说明
- 接入 MethodChannel 实现返回键退后台热启动优化
- 配置自定义签名并更新构建逻辑
- 完善 token 刷新与鉴权失效统一处理
- 新增地图页与截图页路由
- 修复返回键拦截导致的 PopScope 失效问题
2026-08-19 03:43:12 +08:00

19 KiB
Raw Blame History

Flutter 项目规范

严格遵循本文档定义的架构模式、状态管理标准和代码风格。未经授权不得引入新的第三方包或架构模式。优先组合而非继承,业务逻辑不进入 UI 组件。


1. 技术栈

  • 语言: DartSDK ^3.12.2,严格空安全)
  • 框架: Flutter锁定版本,禁止使用 flutter upgrade 自由升级,详见 §11 版本管理
  • 状态管理: flutter_riverpodv2.x注解 + 代码生成)
  • 路由: go_routerv14.x含鉴权重定向详见 §9 路由鉴权
  • 网络请求: dio(配合 json_annotation + freezed 进行 JSON 序列化)
  • 数据模型: freezed + build_runner
  • 本地存储: shared_preferences(非敏感偏好)+ flutter_secure_storage(令牌,平台配置见 §8 安全+ mmkv(高性能键值缓存)
  • 依赖注入: Riverpod Providers不使用 GetIt
  • 国际化: flutter_localizationsl10n / ARB 格式)

⚠️ 版本一致性约束:不同开发者执行 flutter upgrade 的时间不同会导致 SDK 版本漂移引发构建差异。Flutter 与 Dart SDK 版本必须以 CI / .fvmrc 为准,禁止在本地随意升级。


2. 目录结构

采用 特性优先架构Feature-First,特性内部结合 整洁架构Clean Architecture 分层:

lib/
├── main.dart                 # 应用程序入口ProviderScope + runApp
├── app/                      # 全局应用配置
│   ├── app.dart              # CupertinoApp.router 入口
│   ├── router/               # 路由定义GoRouter含 redirect 鉴权)
│   ├── theme/                # 主题、颜色、排版
│   └── constants/            # 全局常量、API 端点、ICE 配置
├── core/                     # 跨特性共享代码
│   ├── network/              # dio 客户端、拦截器链、统一错误模型AppError
│   ├── proto/                # ⚠️ 仅放【跨特性共享】的 Protobuf 生成代码(见 §3 边界规则)
│   ├── storage/              # 本地存储辅助工具shared_preferences / secure_storage / mmkv 封装)
│   ├── utils/                # 辅助函数、扩展方法
│   └── widgets/              # 共享 UI 组件(触摸层)
├── features/                 # 特性模块
│   ├── auth/                 # 登录 / 令牌 / 绑定列表
│   │   ├── data/             # Repository 实现dio
│   │   ├── domain/           # 仓库接口、状态模型freezed
│   │   └── presentation/     # 控制器、登录对话框
│   └── connection/           # 信令 / WebRTC / 控制面板
│       ├── data/             # 信令客户端、WebRTC、编排器、解码器、录制器
│       ├── domain/           # 信令消息、会话状态freezed、【单一特性专用】协议模型
│       └── presentation/     # 控制器、设置页、控制页、鉴权对话框
├── l10n/                     # 国际化(.arb 文件,命名/组织约定见 §10
└── assets/                   # 静态资源(目录结构与命名见 §7 资源管理)
    ├── images/
    └── fonts/

⚠️ core/proto 与特性层边界规则

目录结构中 core/proto 存放 Protobuf 生成代码,但特性层的 domain 又标注"协议模型除外",两者边界容易混淆。明确规则如下:

  • 跨特性共享的 .proto(被两个及以上 feature 使用,或属于全局控制指令协议)→ 放 core/proto/
  • 单一特性专用的协议模型(如仅 connection 使用的信令消息)→ 放 features/xxx/domain/models/,不进 core
  • 判断标准:问一句"去掉这个 feature 后,该 proto 还有人用吗?"没人用就留在 feature 内,保持内聚。

3. 分层职责

表示层(presentation/

UI 组件 + Riverpod Notifier / AsyncNotifier 控制器。

  • 只能通过 ref.watchref.listen 消费状态。
  • 禁止在 onPressedbuild() 中直接调用 API 或编写业务逻辑。
  • 一次性提示通过状态中的 alert 字段传递UI 展示后调用 consumeAlert() 消费。

领域层(domain/

纯 Dart 实体(freezed)和仓库接口定义。

  • 零 Flutter/UI 依赖(单一特性专用的协议模型除外,跨特性共享的协议模型在 core/proto)。
  • 状态类命名避免与 Flutter SDK 冲突(如用 ConnectionSessionState 而非 ConnectionState)。

数据层(data/

实现仓库接口,通过 dio 处理 API 请求,将 JSON DTO 映射为领域实体。


4. 编码规范

状态管理Riverpod

  • 使用 @riverpod 注解 + build_runner 代码生成。
  • keepAlive: true 使用约束(⚠️ 重要)
    • 仅限全局单例级状态(如 AuthSignaling 会话控制器)使用,用于避免页面切换时销毁。
    • 特性页面级控制器禁止使用 keepAlive: true,否则会造成状态残留与内存泄漏。
    • keepAlive: true 意味着 Provider 永远不会自动 dispose,必须配套明确的清理策略:
      • 用户登出 / 断连时主动调用 ref.invalidate(provider)ref.read(provider.notifier).reset()(置 ref.state = null / 初始值防止旧状态token、连接会话残留。
      • 全局单例的清理动作集中在登出流程(auth 控制器或 app 层的统一退出入口)中统一触发。
  • 异步操作优先使用 AsyncNotifierProvider,配合 AsyncValueLoading / Data / Error
  • 业务回调(信令/WebRTC 事件)统一在控制器内映射到状态,不在 UI 层直接持有控制器实例。

数据建模freezed

所有数据类/实体必须用 freezed 实现不可变,并配置 JSON 序列化:

@freezed
class SignalMessage with _$SignalMessage {
  const SignalMessage._();

  const factory SignalMessage({
    String? type,
    String? payload,
  }) = _SignalMessage;

  factory SignalMessage.fromJson(Map<String, dynamic> json) =>
      _$SignalMessageFromJson(json);

  // ⚠️ 安全toString() 直接 jsonEncode 会泄露 token / password 等敏感字段。
  // 若模型不含敏感字段可保留;否则改为手动排除敏感字段,或干脆不覆盖 toString()。
  @override
  String toString() {
    // 示例:排除敏感字段
    final safe = toJson()..remove('token');
    return 'SignalMessage(${safe.toString()})';
  }
}

⚠️ 敏感信息防护jsonEncode(toJson()) 形式的 toString() 会把 token、password 等明文写入日志 / 崩溃上报。规范约定:

  1. 含敏感字段的模型 不得 使用 jsonEncode(toJson()) 形式的 toString()
  2. 改为手动排除敏感字段后输出,或直接不覆盖 toString()(依赖调试器查看字段);
  3. 若确需调试输出,标注 @visibleForTesting 并约定生产环境不调用。

UI 组件

  • 尽可能使用 const 构造函数,避免不必要的重建。
  • 复杂子树提取为独立私有/公有无状态组件,不要写冗长的内联辅助方法。
  • 响应式适配使用 LayoutBuilder / MediaQuery 或项目统一的屏幕适配工具。

5. 命名约定

类别 规范 示例
文件/文件夹 snake_case.dart connection_controller.dart
类/枚举 PascalCase ConnectionSessionState
变量/方法 camelCase sendResolutionChange()
Provider Provider 结尾 authRepositoryProvider
私有成员 _ 前缀 _handleSignalMessage()

6. 代码生成

修改或新增带代码生成的模型/控制器后,运行:

# 一次性生成freezed / json_serializable / riverpod_generator
dart run build_runner build --delete-conflicting-outputs

# 监听模式
dart run build_runner watch --delete-conflicting-outputs

# 生成国际化l10n.yaml 已配置)
flutter gen-l10n

7. 资源管理

静态资源统一放在项目根 assets/ 下,按类型分子目录:

assets/
├── images/                  # 图片资源
│   ├── common/              # 通用图标 / 占位图
│   └── feature_xxx/         # 按特性隔离的图片
└── fonts/                   # 自定义字体
  • 图片命名snake_case,带用途/状态后缀,如 ic_back.pngbg_login_dark@2x.png;分辨率变体使用 @2x / @3x 后缀。
  • pubspec.yaml 引入:在 flutter.assets 中显式声明目录(当前模板已注释示例,新增资源后需补齐),字体在 flutter.fonts 中声明 familyasset
  • 超过 ~100KB 的图片优先走 CDN / 网络加载,避免包体积膨胀。

8. 安全

  • flutter_secure_storage 平台配置
    • iOS:数据落地于 Keychain(系统级加密,不随 app 卸载必然清除,需走钥匙串共享组或登录项)。
    • Android:默认使用 EncryptedSharedPreferences(需 minSdkVersion >= 23;低于此需自定义 AndroidOptionsencryptedSharedPreference 开关)。
    • 仅存放 refreshToken 等高危凭证,禁止存明文密码。
  • 敏感字段防护:见 §4 数据建模中 toString() 的约束token / password 不得进入日志与崩溃上报。
  • 本地偏好:非敏感配置(主题、语言)用 shared_preferences;高频读写缓存可用 mmkv
  • 网络传输:所有 API 走 HTTPS信令 WebSocket 使用 wss://

9. 路由鉴权

基于 go_router 实现统一的鉴权重定向:

  • 受保护路由:在 app/router/ 中集中定义路由表,受保护路由(如 connection/*)标记为需鉴权。
  • 未登录重定向:通过 GoRouterredirect 回调检查登录态(读取 auth 控制器的 AsyncValue / 本地 token。未登录时 redirect/login,并把当前 location 写入 extra 或 query 参数。
  • 登录回跳:登录成功后读取 extra 中的目标路由,调用 context.go() 回跳原页面;无目标时默认跳转首页。
  • 登出清理联动:登出时除 §4 的 Provider 清理外,路由需 go('/login') 清空导航栈。

10. 国际化l10n

采用 ARB 格式,由 l10n.yaml 驱动 flutter gen-l10n 生成 AppLocalizations

  • 文件命名 / 组织:语言文件以 intl_<locale>.arb 命名(如 intl_zh.arbintl_en.arbtemplate-arb-file 指定模板语言(当前为 intl_zh.arb)。
  • key 命名snake_case 语义化,带上下文前缀避免冲突,如 login_titleconnection_video_off
  • 占位符:使用 {param} 占位,对应生成的 getter 会带参数;多语言文案需保持占位符一致。
  • 复数 / 选择:使用 ARB 的 plural / select 语法(如 @count + plural),避免手动拼接。
  • 维护约定:新增文案须同时更新所有语言 ARB缺失的 key 以模板语言文案兜底。

11. 版本管理

  • 锁定 Flutter / Dart 版本:在 README 与 CI 中明确记录 flutter --version 对应的版本号。
  • 推荐使用 FVM:项目根放置 .fvmrc,团队成员统一 fvm use,禁止本地自由 flutter upgrade
  • pubspec.yamlenvironment.sdk 已锁定为 ^3.12.2,依赖版本尽量用 caret 范围并定期 flutter pub outdated 审查。

12. 错误处理统一规范

本规范已在 lib/core/network/ 落地,新增网络请求必须复用以下基础设施,禁止在 Repository 中自行创建 Dio 实例或手写 try/catch 转换异常。

  • 统一错误模型 AppErrorapi_error.dart,含 typenetwork / unauthorized / forbidden / server / business / unknowncodestatusCodemessageoriginal,以及 isUnauthorized / isRetryable 判定,与统一用户文案 userMessage)。数据/网络层统一向上抛 AppError 而非裸 Exception / DioException
  • 兼容异常 ApiExceptionapi_exception.dart):保留 code / message 字段以兼容既有调用方;新增代码建议直接使用 AppErrorDioClient 对外统一抛出 ApiException
  • dio 拦截器链(归属 core/network/dio_client.dart
    1. _AuthInterceptor:注入 Authorization: Bearer <token>
    2. _ResponseInterceptor:统一解析 { code, message, data }code == 0 提取 data,否则抛业务错误。
    3. _ErrorInterceptor:将 DioException 转换为 ApiException401 时自动用 refreshToken 刷新并重试一次,刷新失败则向下传递 unauthorized 错误。
  • 统一请求入口DioClient.get/post/put/delete 已封装Repository 直接调用并捕获 ApiException,无需重复 try/catch。
  • 错误分类与处理策略
    • 401_ErrorInterceptor 自动刷新 token 重试;刷新失败 → 控制器检测到 unauthorized 触发登出清理。
    • 403 → 无权限,展示无权限提示。
    • 4xx 其他 / 业务错误 → 展示后台 message
    • 5xx / 网络不可达 → 提示重试UI 提供 retry 入口)。
  • 统一文案映射:所有控制器/UI 捕获异常后必须通过 userMessageOf(e)error_message.dart)获取展示文案,禁止直接 toString()(防敏感信息泄露)。
  • UI 呈现AsyncValue 的 Error 分支统一渲染——全局错误(如 401 登出)走 alert/Toast局部错误走内联错误 + retry 按钮,不强制全局错误页。

13. 测试规范

  • 目录镜像test/ 下按 test/features/<feature>/test/core/ 镜像 lib/ 结构。
  • Mock 规范:使用 ProviderContainer + overrides 覆盖 Riverpod ProviderRepository / dio 用 mockito 或手写 fakeWebRTC / 信令用桩对象。
  • 分层测试:领域层纯函数单测;控制器用 Container override 后 read(notifier) 驱动并断言 stateUI 用 pumpWidget(ProviderScope(...))
  • 运行门槛:提交前 flutter test 需通过(见 §14

14. Git / CI

  • Commit Message:遵循 Conventional Commitsfeat: / fix: / refactor: / docs: / test: / chore:)。
  • PR 模板含变更说明、影响范围、测试步骤、截图UI 变更)。
  • CI 流水线(建议):
    1. flutter analyze 无 issue。
    2. flutter test 通过。
    3. dart run build_runner build --delete-conflicting-outputs 校验生成代码最新。
    4. 多端构建Android / iOS / Web产物校验。
  • 协议同步.proto / 信令 JSON 变更须同步 Android / iOS / Web 各端,并在 PR 中标注。

15. 完成标准

  • 新文件遵循特性优先结构app / core / features / l10n / assets
  • 代码为空安全、完全类型化,并进行 const 优化。
  • 对应创建数据层、领域层和表示层组件。
  • keepAlive: true 的控制器已配置登出/断连清理策略(见 §4
  • 含敏感字段的模型已处理 toString() 泄露风险(见 §4、§8
  • 提交前 flutter analyze 无 issueflutter test 通过。
  • 协议(.proto / 信令 JSON变更需同步 Android / iOS / Web 各端。

16. C 端认证接口对接约定open 模块)

本节记录 Flutterttstd_family_care)与后端 youlai-boot-ttstd 的 C 端认证接口契约, 对接 open 模块(com.youlai.boot.open),账号落地 app_user 表,与后台管理端 sys_user 物理分表隔离。

16.1 基础地址

AppConstants.kBaseUrl 为后端 open 模块根路径,已包含 /api/v1/open/ 前缀

static const String kBaseUrl = 'http://<host>:<port>/api/v1/open/';

仓库层请求路径(如 /login)会拼接到该 BaseUrl 之后,即实际请求 /api/v1/open/auth/login不要/api/v1/open//auth 再拼进 path。

⚠️ 前端禁止将 BaseUrl 改为 /api/v1/sn/ 前缀。/api/v1/sn/** 由后端 MobileApiSignatureFilter 强制校验设备签名四件套(X-Device-SN / X-Nonce / X-Timestamp / X-Sign),仅用于设备被控端通信C 端用户登录走 open 模块, 不经过该过滤器。若误用 /sn/ 前缀,会返回 A0801 设备标识不能为空 等错误。

16.2 接口端点POST

所有请求均携带 Authorization: Bearer <token>(登录/发验证码等匿名接口可省略)。

功能 路径 请求参数 说明
账号密码登录 /login body {username, password} username 为手机号或用户名
验证码登录 /login/mobile body {mobile, code} 需先调用"发送登录验证码"
发送登录验证码 /login/sms/code query mobile 验证码存 Redis5 分钟有效
注册 /register/mobile body {mobile, code, password} 注册成功即签发令牌
发送注册验证码 /register/sms/code query mobile 手机号未注册时发送
重置密码 /reset-password body {mobile, code, password} 需先调用"发送重置密码验证码"
发送重置密码验证码 /reset-password/sms/code query mobile 手机号必须已注册
刷新令牌 /refresh-token query refreshToken 换取新访问令牌
退出登录 /logout query accessToken(可选) 注销令牌

16.3 响应结构

  • 成功:{ "code": 0, "message": "...", "data": ... }
  • 失败:code != 0message 为错误提示(由 _ResponseInterceptorApiException)。

登录/注册/刷新成功时 data 为后端 AuthenticationToken

{ "tokenType": "Bearer", "accessToken": "xxx", "refreshToken": "yyy", "expiresIn": 7200 }

前端 LoginResult.fromJson 已兼容解析 accessToken(旧字段 token 作兜底)。

16.4 对应实现位置

  • 数据层:lib/features/auth/data/auth_repository_impl.dart
  • 令牌持久化:lib/core/storage/token_storage.dart
  • 统一请求/拦截器:lib/core/network/dio_client.dart
  • 领域模型:lib/features/auth/domain/auth_models.dartLoginResultSmsScene

16.5 场景映射

SmsScene 决定发送验证码的后端路径,仓库层按场景分流:

SmsScene 后端路径 触发页面
login /login/sms/code 登录页(验证码登录)
register /register/sms/code 注册页
resetPassword /reset-password/sms/code 忘记密码页

17 解耦适配

import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';

都变更为

import 'package:material_ui/material_ui.dart';
import 'package:cupertino_ui/cupertino_ui.dart';

flutter_localizations也被拆分Material和Cupertino的本地化委托分别移进了对应的包。

迁移前:

import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter/material.dart';

localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
  GlobalCupertinoLocalizations.delegate,
  GlobalMaterialLocalizations.delegate,
  GlobalWidgetsLocalizations.delegate,
],

迁移后:

import 'package:material_ui/material_ui.dart';

localizationsDelegates: GlobalMaterialLocalizations.delegates,