Flutter 睡眠 App 登录功能梳理(手机号验证码 + Token 会话)

本文基于 Flutter 业务工程通读整理,说明手机号 + 短信验证码登录的完整链路:涉及哪些页面、字段、本地存储、HTTP 鉴权,以及登录态在全局如何被消费。文中 API 主机与包名均用占位符,便于公开发布。


1. 登录方式概览

项目 实现
认证方式 手机号 11 位 + 6 位短信验证码
协议 登录页勾选《用户协议》《隐私政策》(URL 来自通用配置接口)
会话凭证 服务端返回 token,本地持久化
自动登录 Splash 判断 token 非空 → 进入主应用,否则进登录页
退出 仅本地清空 token/用户对象,服务端 logout 接口调用

2. 端到端流程

sequenceDiagram
  participant U as 用户
  participant Splash as Splash
  participant Login as LoginView
  participant API as LoginServiceApi
  participant Store as UserInfoUtils
  participant Main as MainApp

  U->>Splash: 启动 App
  Splash->>Store: getToken()
  alt token 非空
    Splash->>Main: pushReplacement
  else token 为空
    Splash->>Login: pushReplacement
  end

  U->>Login: 输入手机号
  Login->>API: GET common/sendSecurityCode?phone=
  U->>Login: 验证码 + 勾选协议
  Login->>API: GET app/login?phone=&securityCode=
  API-->>Login: code=200, data=UserInfoBeanEntity
  Login->>Store: saveUserInfo / saveToken / saveUserId / saveHeadImage
  Login->>Main: pushReplacement

  Note over Main: 后续请求经 HttpParamsInterceptor 带 token

与 Splash 隐私弹窗的区别

  • KEY_PRIVACY:首次启动隐私指引(main.dart),同意后才进入倒计时跳转。
  • 登录页 isCheck:用户协议勾选,与隐私弹窗是两套合规逻辑。

3. 核心文件

文件 职责
lib/ui/LoginView.dart 登录 UI、60s 验证码倒计时、登录/发码、拉通用配置
lib/ui/service/LoginServiceApi.dart 发码、登录、getComConfig
lib/utils/UserInfoUtils.dart 内存缓存 + SharedPreferences
lib/utils/LocalStorage.dart SP 封装
lib/config/BaseConstants.dart 存储 Key
lib/net/interceptor/HttpInterceptor.dart 注入 access_tokenuserIddevice
lib/main.dart Splash、隐私、按 token 分流
lib/MainApp.dart 监听 LogOut / ReenterEvent → 回 Splash
lib/ui/my/UserInfoView.dart 资料编辑、退出/注销

4. 服务端接口

常量见 lib/config/ApiConstants.dart,调用见 LoginServiceApi

接口 路径常量 HTTP 参数
发送验证码 URL_SEND_SECURITY_CODEcommon/sendSecurityCode GET phone
登录 URL_LOGINapp/login GET phone, securityCode
通用链接 URL_GET_RELATEDLINKScommon/getRelatedLinks GET

基址由 BaseApi.getUrl 决定(ApiConstants.isDeBug 切换测试/正式环境,如 https://api.example.com/)。

登录成功响应BaseResultBeanEntitycode == 200):

{
  "code": 200,
  "message": "成功",
  "data": {
    "token": "YOUR_ACCESS_TOKEN",
    "userInfo": {
      "userId": "user-id-string",
      "phone": 13800138000,
      "nickName": "昵称",
      "headImage": "https://cdn.example.com/avatar.png",
      "birthDay": "1990-01-01",
      "sex": 1,
      "age": 30,
      "registTime": "...",
      "updateTime": "..."
    }
  }
}

5. 数据模型与字段

5.1 UserInfoBeanEntity

定义:lib/ui/bean/UserInfoBeanEntity.dart,JSON 映射:UserInfoBeanEntity.g.dart

字段 类型 说明
token String 访问令牌
userInfo UserInfoBeanUserInfo? 用户资料嵌套对象

5.2 UserInfoBeanUserInfo

字段 JSON 键 类型 典型用途
userId userId String? 业务主键、H5 query、日志
phone phone int? 登录页回填、我的页脱敏展示
nickName nickName String? 昵称
headImage headImage String? 头像 URL
birthDay birthDay String? 生日
sex sex int? 性别
age age int? 年龄
registTime registTime String? 注册时间
updateTime updateTime String? 更新时间

5.3 ComConfigBeanEntity(非登录响应,登录前拉取)

字段 本地 Key 用途
instructionVideoLink KEY_VIDEOLINK 说明视频
webStoreLink KEY_WEBSTORELINK 商城 H5
termsOfUseLink KEY_TERMSOFUSELINK 登录页用户协议
privacyPolicyLink KEY_PRIVACYPOLICYLINK 登录页隐私政策

6. 本地存储

通过 UserInfoUtils 写入 LocalStorage(SharedPreferences):

Key 常量 存储内容 登录成功时
KEY_USER_INFO 完整 UserInfoBeanEntity JSON saveUserInfo
KEY_TOKEN token 字符串 saveToken
KEY_USERID userId 字符串 saveUserId
KEY_HEADIMAGE 头像 URL saveHeadImage
KEY_DEVICEINFO 机型/系统/版本拼接 ✅ 进登录页 getDeviceInfo
KEY_PRIVACY 是否同意启动隐私弹窗 Splash,非登录勾选

内存缓存mTokenmUserIdmHeadImagemDeviceuserInfoBeanEntity,减少重复读 SP。

getToken() 回退顺序

  1. 内存 mToken
  2. KEY_TOKEN
  3. getUserInfo()!.token

7. 登录成功后的写入代码

LoginView.login(节选):

final str = await LoginServiceApi().login(phone, code);
final baseResult = BaseResultBeanEntity.fromJson(str);
if (baseResult.code == 200) {
  var userInfoBeanEntity =
      jsonConvert.convert<UserInfoBeanEntity>(baseResult.data);
  UserInfoUtils.saveUserInfo(userInfoBeanEntity);
  UserInfoUtils.saveToken(userInfoBeanEntity.token.toString());
  UserInfoUtils.saveHeadImage(userInfoBeanEntity.userInfo!.headImage!);
  UserInfoUtils.saveUserId(userInfoBeanEntity.userInfo!.userId.toString());
  RouterUtils.pushReplacement(context, const MainApp());
}

8. HTTP 鉴权(登录之后)

HttpParamsInterceptor 在每个 BaseApi 请求 onRequest 中设置 Header:

Header 来源 为空时
access_token getToken() "11"
userId getUserId() "11"
device getDevice() + userId 拼接 "11"
source 固定 "1"

注意:token 清空后仍可能带占位 "11",依赖服务端拒绝非法会话。ErrorInterceptor 统一处理 401 跳转登录。


9. 启动分流

main.dart_SplashAppViewState._jumpRoutePage

token = UserInfoUtils.getToken();
if (token.isNotEmpty) {
  RouterUtils.pushReplacement(context, const MainApp());
} else {
  RouterUtils.pushReplacement(context, const LoginView());
}

仅判断 token,不单独校验 userId


10. 退出与重新登录

flowchart LR
  A[UserInfoView 退出/注销] --> B[saveToken 空字符串]
  A --> C[saveUserInfo 空对象]
  A --> D[fireEvent LogOut]
  D --> E[MainApp pushReplacement Splash]
  E --> F{token 空?}
  F -->|是| G[LoginView]
操作 位置 服务端
退出登录 showExitLoginDialog
注销 showLogoutDialog ❌(文案不同,逻辑同退出)
LogOut MainApp 监听 回 Splash
ReenterEvent MainApp 监听 回 Splash

退出时 UserInfoUtils.clean() 被注释,蓝牙 MAC、范式等 SP 项可能仍保留。会调用 BluetoothUtils.disconnectCurBle()


11. 登录后资料编辑(UserServiceApi)

入口:NewMyUserInfoView。均需已登录 Header。

功能 请求 本地
昵称 PUT app/editUser?nickName= 更新 userInfosaveUserInfo
性别 PUT app/editUser?sex= 同上
生日 PUT app/editUser?birthDay= 同上
头像 OSS 上传 → PUT 带头像 URL saveHeadImage + saveUserInfo

成功后 EventBusUpdataUserEventMyUserView 刷新我的页头部。


12. userId / token 在工程中的使用

12.1 强依赖 userId(拼 URL 或业务 ID)

模块 文件 用法
睡眠日记 H5 SleepNote.dart query userId
问卷/游戏 QuestionNaireView.dart, GameIntroduceView.dart getUserId()
设备帮助 DeviceHelpListView.dart 常见问题 H5
枕垫 API pillow_serviceApi.dart 用户标识
日志 FileWriteLog.dart, main.dart 崩溃/操作日志
闹钟 AlarmSettingOffOnPage.dart 多处

12.2 凡走 BaseApi 的模块

HomeServiceApiReportsServiceApiDevicesServiceApiUserServiceApiSnoreServiceApi 等——统一依赖拦截器中的 token + userId + device

12.3 展示层

位置 数据
MyUserView 脱敏手机号、头像、性别
LoginView init 缓存用户预填手机号

13. LoginServiceApi 其它方法(登录态复用)

方法 接口 主要调用方
getComConfig common/getRelatedLinks LoginView, MainApp
getUserRemind 用户提醒 HomeX8View
getIndexBanner 首页 Banner HomeX8View
getHomeModuleList 首页模块 Home
getHospitalAddress 医院列表 HomeSleep

14. 相关页面一览

页面 路径 与登录关系
Splash lib/main.dart token 分流
LoginView lib/ui/LoginView.dart 唯一登录页
MainApp lib/MainApp.dart 登录后主容器
NavigationBottom lib/NavigationBottom.dart 三 Tab,需登录态
UserInfoView lib/ui/my/UserInfoView.dart 资料、退出
ComWebView lib/ui/ComWebView.dart 协议 H5
业务页 报告/首页/设备等 假定已登录

15. 重要程度与分类

等级 能力
P0 发码、登录、存 token/userId、Splash 分流、HttpParamsInterceptor
P1 通用配置(协议链接)、UserInfoView 资料编辑
P2 各业务 H5 带 userId;token 空时 Header 占位行为
分类 内容
服务器 发码、登录、editUser、getRelatedLinks;登录后全部 API
本地 SP 持久化、隐私 KEY、退出清 token
硬件 退出时断 BLE,与会话独立

16. 实现注意点

  1. 发码与登录均为 GET,参数在 query。
  2. 自动登录只看 token,不看 userId。
  3. 无服务端 logout;安全依赖 token 失效策略。
  4. UserServiceApi.editUserHead 等 PUT 路径在代码中与 URL_EDITUSER_SEX 共用,联调时需与后端路径一致。
  5. UserGameBeanEntity 仅有 JSON 基础设施,不参与 App 登录流程;小游戏 H5 直接用 getUserInfo()?.userInfo?.userId

17. 快速定位

需求 位置
登录 UI 校验 LoginView.dart 约 246–570 行
存 token LoginView.login + UserInfoUtils
是否已登录 main.dart _jumpRoutePage
请求头 token HttpInterceptor.dart
退出 UserInfoView 739–771 行;MainApp LogOut 监听
字段定义 UserInfoBeanEntity.dart

小结

登录链路是 验证码登录 → 持久化 token 与 userInfo → Splash 按 token 分流 → 全 App 经拦截器带 access_tokenuserId 除 Header 外还广泛用于 H5 与日志。退出仅清本地会话并回 Splash,不调用注销 API。理解 UserInfoUtilsHttpParamsInterceptor 是排查「登录后接口 401 / 用户错乱」的第一入口。