Flutter+OpenHarmony 智能家居开发 Day12|用户中心 + 数据统计 + 鸿蒙原子化服务适配全流程
本文将从用户中心(登录 / 注册 / 个人设置)、数据统计(多维度统计 + 可视化图表)、鸿蒙原子化服务(提取 / 适配 / 打包)、权限适配、性能优化五个核心模块展开,每一步都讲清「原理、实现、踩坑、解决方案」,代码可直接复用,兼顾新手入门与工程化实践,严格满足 2 万字详实内容要求。,同时完善权限管理、优化 APP 性能,让 APP 从 “流畅可用” 升级为 “生态兼容、数据驱动、安全可控”,
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
大家好~经过 Day11 的开发,我们的 Flutter+OpenHarmony 智能家居 APP 已经完成了「全局底部选项卡整合、设备详情页完善、鸿蒙多端布局适配」三大核心任务,实现了页面统一入口、功能高频补充、多终端兼容,APP 从 “功能可用” 正式升级为 “体验流畅”。但此时 APP 仍存在三个关键短板:一是缺乏用户身份认证体系,无法实现多设备登录、个人数据同步,不符合商业化应用标准;二是缺乏数据统计能力,无法直观展示设备使用、场景执行、定时触发等核心数据,用户难以掌握家居使用情况;三是未适配鸿蒙核心特色能力 —— 原子化服务,无法实现 “无需安装、一键启动” 的便捷体验,脱离鸿蒙生态核心优势。
这就是 Day12 要解决的核心问题:搭建完整用户中心模块、实现全维度数据统计与可视化、完成鸿蒙原子化服务适配,同时完善权限管理、优化 APP 性能,让 APP 从 “流畅可用” 升级为 “生态兼容、数据驱动、安全可控”,贴合开源鸿蒙跨平台应用的商业化、生态化需求,为 Day13 的多设备验证、打包发布做好全面准备。
Day12 完全延续前 11 天「逐步骤拆解 + 逐行代码解析 + 全场景踩坑解决」的写作风格,100% 适配 CSDN 文章发布格式,所有代码块采用标准 Markdown 格式(dart/json/```yaml),直接复制即可高亮显示,无任何特殊标签、无文件生成形式、无冗余内容,全程围绕 Flutter+OpenHarmony 跨平台开发落地。本文将从用户中心(登录 / 注册 / 个人设置)、数据统计(多维度统计 + 可视化图表)、鸿蒙原子化服务(提取 / 适配 / 打包)、权限适配、性能优化五个核心模块展开,每一步都讲清「原理、实现、踩坑、解决方案」,代码可直接复用,兼顾新手入门与工程化实践,严格满足 2 万字详实内容要求。
本文承接 Day3-Day11 完整项目架构,重点聚焦「用户体系搭建、数据价值挖掘、鸿蒙生态适配」,是开源鸿蒙跨平台应用从 “个人 demo” 走向 “商业化产品” 的关键一步,学完本篇,你的 APP 将具备用户身份认证、数据统计分析、鸿蒙原子化服务三大核心能力,完全贴合开源鸿蒙生态要求,可直接对接 Day13 的多设备验证与打包发布。
一、Day12 核心目标(方向清晰,不做无用功)
Day12 在已有核心功能基础上,聚焦「用户、数据、生态」三大维度,所有开发工作围绕以下 18 项核心目标推进,确保功能落地、体验优秀、生态兼容、安全可控:
- 搭建完整用户中心模块,支持手机号登录 / 注册、密码找回、验证码校验,实现用户身份认证;
- 开发个人中心页面,支持头像上传、昵称修改、密码修改、个人信息展示,满足用户个性化需求;
- 实现用户数据本地持久化 + 缓存管理,支持登录状态持久化,重启 APP 无需重复登录,退出登录清除缓存;
- 搭建全维度数据统计体系,统计设备使用时长、场景执行次数、定时触发次数、异常发生次数四大核心维度;
- 实现数据统计可视化,通过折线图、柱状图、饼图展示统计数据,支持按时间筛选(今日 / 本周 / 本月 / 自定义);
- 适配鸿蒙原子化服务,提取 APP 核心功能(设备快捷控制、场景一键执行),实现无需安装 APP,快速启动核心服务;
- 完成鸿蒙原子化服务配置、页面开发、打包调试,确保在鸿蒙设备上可正常启动、使用、跳转主 APP;
- 扩展本地数据库(ObjectBox),新增用户信息、数据统计记录实体,实现用户数据、统计数据本地持久化;
- 完善权限管理,新增用户隐私权限(头像存储权限、手机号授权权限),适配鸿蒙隐私保护规范,处理权限拒绝场景;
- 实现用户数据与设备、场景、定时任务数据关联,确保不同用户登录后仅查看自己的设备、场景、定时任务;
- 优化统计图表性能,解决图表加载卡顿、数据刷新缓慢、多端显示错乱问题,适配手机、平板、DAYU200 开发板;
- 实现原子化服务与主 APP 的数据共享,确保原子化服务操作后,主 APP 数据实时同步;
- 处理用户中心全场景异常:登录失败、注册失败、验证码获取失败、头像上传失败、密码修改失败;
- 处理数据统计异常:统计数据为空、数据计算错误、图表渲染失败;
- 处理原子化服务异常:启动失败、权限不足、数据同步失败、无法跳转主 APP;
- 优化 APP 整体性能,减少内存占用、降低功耗,确保 DAYU200 开发板长时间运行无卡顿、无闪退;
- 规范工程结构,新增用户中心、数据统计、原子化服务相关模块,与原有模块解耦,便于后续扩展维护;
- 完成 Day12 核心功能整合测试,确保用户中心、数据统计、原子化服务与原有功能(设备、场景、定时)联动正常。
二、前置准备(无缝衔接 Day11,不脱节)
Day12 需要新增 3 类核心依赖(用户中心、图表统计、原子化服务相关),同时确认已有能力、工程结构、鸿蒙适配环境就绪,避免开发过程中出现编译报错、功能失效等问题,确保开发流程顺畅。
2.1 已有能力回顾(必须确认)
- 数据层:BaseDevice / 空调 / 灯光 / 窗帘模型、SmartScene 场景模型、TimerTask 定时任务模型、DeviceOperationLog 日志模型、DeviceAlertRecord 异常模型,ObjectBox 本地数据库支持全量 CRUD;
- 通信层:MQTT 实时通信、批量指令下发、设备状态双向同步,支持定时触发、场景执行的 MQTT 指令推送;
- 业务层:DeviceProvider 全局状态管理(设备、场景、定时、日志、异常),定时引擎、通知管理器、鸿蒙多端适配工具正常运行;
- UI 层:全局底部选项卡、首页聚合页、设备列表页、设备详情页、场景列表页、定时列表页,具备完善的 UI 展示与交互功能;
- 权限层:鸿蒙网络、存储、后台保活、通知、悬浮窗、唤醒锁等权限已配置,确保后台运行、通知推送正常;
- 鸿蒙适配:已完成页面多端适配(手机 / 平板 / DAYU200 开发板),底部选项卡、设备详情页、首页聚合页在多终端布局正常。
2.2 新增核心依赖(直接复制到 pubspec.yaml)
Day12 需要新增用户登录、图表统计、原子化服务、头像处理相关依赖,均已完成 OpenHarmony 适配,直接复制以下配置,保留原有依赖不动,避免版本冲突:
yaml
dependencies:
flutter:
sdk: flutter
# 核心基础依赖(Day3-Day11,无需修改)
dio: ^5.4.0
json_annotation: ^4.8.1
provider: ^6.1.1
get_it: ^7.2.0
logger: ^1.1.0
device_info_plus: ^9.1.0
flutter_windowmanager: ^0.2.0
permission_handler: ^11.0.1
shared_preferences: ^2.2.2
# 本地持久化依赖(无需修改)
objectbox: ^2.0.0
objectbox_flutter_libs: ^2.0.0
# 网络与通信依赖(无需修改)
connectivity_plus: ^5.0.2
mqtt_client: ^10.0.0
crypto: ^3.0.3
# 定时与通知依赖(无需修改)
flutter_local_notifications: ^16.1.0
workmanager: ^0.5.1
timezone: ^0.9.2
flutter_native_timezone: ^2.0.0
# 新增:Day12核心依赖(用户中心+图表+原子化服务)
# 用户中心相关(登录/注册/头像)
flutter_svg: ^2.0.9 # SVG图标支持(个人中心图标)
pin_code_fields: ^8.0.1 # 验证码输入框
image_picker: ^1.1.0 # 头像选择/拍摄
path_provider: ^2.1.2 # 头像本地存储路径
encrypt: ^5.0.3 # 密码加密存储(安全)
# 数据统计图表相关
fl_chart: ^0.55.2 # 可视化图表(折线图/柱状图/饼图)
intl: ^0.19.0 # 日期格式化(统计时间筛选)
# 鸿蒙原子化服务相关
ohos_atom_service: ^1.0.0 # 鸿蒙原子化服务适配插件
url_launcher: ^6.2.5 # 原子化服务跳转主APP
dev_dependencies:
flutter_test:
sdk: flutter
# 原有开发依赖(无需修改)
json_serializable: ^6.7.1
build_runner: ^2.4.6
flutter_lints: ^2.0.0
objectbox_generator: ^2.0.0
2.3 执行依赖安装命令
在项目根目录执行以下命令,确保所有依赖(原有 + 新增)正常加载,重点检查鸿蒙原子化服务、图表、头像处理相关依赖是否下载成功:
bash
运行
flutter pub get
若出现依赖冲突,先执行flutter clean清除缓存,再重新执行flutter pub get;若鸿蒙平台编译失败,升级 Flutter for OpenHarmony 插件至最新版本,同时确认ohos_atom_service插件版本与鸿蒙 SDK 版本(API10+)兼容。
2.4 鸿蒙权限补充配置(关键必做)
Day12 新增用户隐私权限(头像存储、手机号授权)和原子化服务相关权限,需在entry/src/main/module.json5中补充配置,确保权限齐全,避免功能失效:
json
{
"requestPermissions": [
// 原有权限(Day3-Day11,无需修改)
{"name": "ohos.permission.INTERNET"},
{"name": "ohos.permission.STORAGE"},
{"name": "ohos.permission.KEEP_BACKGROUND_RUNNING"},
{"name": "ohos.permission.NOTIFICATION"},
{"name": "ohos.permission.SYSTEM_FLOAT_WINDOW"},
{"name": "ohos.permission.WAKE_LOCK"},
{"name": "ohos.permission.RECEIVE_BOOT_COMPLETED"},
// 新增:Day12用户中心相关权限
{"name": "ohos.permission.READ_MEDIA"}, // 读取相册(头像选择)
{"name": "ohos.permission.CAMERA"}, // 相机(头像拍摄)
{"name": "ohos.permission.READ_PHONE_STATE"}, // 读取手机号(授权登录)
// 新增:Day12原子化服务相关权限
{"name": "ohos.permission.ATOMIC_SERVICE"}, // 原子化服务基础权限
{"name": "ohos.permission.GET_BUNDLE_INFO"}, // 获取应用信息(跳转主APP)
{"name": "ohos.permission.DISTRIBUTED_DATASYNC"} // 分布式数据同步(原子化与主APP数据共享)
],
// 新增:原子化服务基础配置(后续详细配置)
"atomicService": {
"enable": true,
"mainAbility": "com.example.smart_home.AtomServiceMainAbility",
"icon": "$media:icon",
"label": "智能家居原子化服务",
"description": "无需安装,快速控制智能设备、执行场景",
"categories": ["action.view"]
}
}
2.5 工程结构扩展(统一规范,便于维护)
在原有工程结构基础上,新增用户中心、数据统计、原子化服务相关模块,严格按分层架构放置,与原有模块解耦,确保工程结构清晰、便于后续扩展维护,具体结构如下:
plaintext
lib/
├── core/
│ ├── mqtt/ // 原有MQTT工具
│ ├── objectbox/ // 原有本地数据库(新增用户、统计相关操作)
│ ├── constants/ // 新增用户、统计、原子化服务相关常量
│ ├── timer/ // 原有定时任务引擎
│ ├── notification/ // 原有通知管理工具
│ ├── adapter/ // 原有鸿蒙多端布局适配工具(新增图表、原子化适配)
│ ├── user/ // 新增:用户中心工具(密码加密、验证码、头像处理)
│ ├── statistics/ // 新增:数据统计工具(数据计算、图表适配)
│ └── atom_service/ // 新增:鸿蒙原子化服务工具(配置、跳转、数据共享)
├── data/
│ ├── models/ // 新增:用户模型、统计模型、登录注册请求/响应模型
│ ├── repositories/ // 扩展:设备/场景仓库新增用户关联、统计数据查询方法
│ └── api/ // 新增:用户登录/注册模拟API(后续可扩展真实接口)
├── domain/
│ └── providers/ // 新增:UserProvider(用户状态管理)、StatisticsProvider(统计状态管理)
└── ui/
├── pages/
│ ├── main/ // 原有:底部选项卡主页面(新增个人中心Tab)
│ ├── home/ // 原有:首页聚合页(新增用户信息展示、统计入口)
│ ├── device/ // 原有:设备列表页、设备详情页(关联用户数据)
│ ├── scene/ // 原有:场景列表页(关联用户数据)
│ ├── timer/ // 原有:定时列表页(关联用户数据)
│ ├── device_log/ // 原有:设备日志、异常记录页面(关联用户数据)
│ ├── user/ // 新增:用户中心相关页面(登录、注册、个人中心、个人设置)
│ ├── statistics/ // 新增:数据统计页面(总览、设备使用、场景执行、定时触发、异常统计)
│ └── atom_service/ // 新增:鸿蒙原子化服务相关页面(快捷控制、场景执行)
└── widgets/
├── tab_bar/ // 原有:底部选项卡组件(新增个人中心Tab)
├── home/ // 原有:首页聚合页组件
├── device_detail/ // 原有:设备详情页组件
├── common/ // 原有:通用交互组件(新增验证码、头像上传组件)
├── user/ // 新增:用户中心组件(登录表单、注册表单、头像组件)
├── statistics/ // 新增:统计图表组件(折线图、柱状图、饼图)
└── atom_service/ // 新增:原子化服务组件(快捷设备卡片、场景快捷按钮)
2.6 核心设计原则(先看懂再开发)
- 用户中心设计原则:安全性(密码加密存储)、易用性(简洁登录 / 注册流程)、个性化(头像 / 昵称修改),支持登录状态持久化、退出登录清除缓存,不同用户数据隔离;
- 数据统计设计原则:全维度(覆盖设备、场景、定时、异常)、可视化(图表直观展示)、可筛选(按时间筛选),数据实时计算、本地持久化,避免重复计算;
- 原子化服务设计原则:轻量化(仅提取核心功能)、便捷性(无需安装、一键启动)、数据同步(与主 APP 数据互通),适配鸿蒙原子化服务规范,支持跳转主 APP;
- 多端适配原则:用户中心、统计图表、原子化服务页面均采用自适应布局,适配手机、平板、DAYU200 开发板,重点优化开发板的图表显示、原子化服务启动速度;
- 数据关联原则:用户数据与设备、场景、定时、日志、异常数据关联(通过用户 ID),确保不同用户登录后仅查看、操作自己的相关数据;
- 安全性原则:用户密码采用加密存储(不明文存储),验证码校验、手机号校验严格,避免非法登录;头像存储在本地私有目录,保护用户隐私;
- 性能优化原则:统计数据懒加载、图表数据分页加载,减少内存占用;原子化服务减少资源依赖,提升启动速度;用户缓存合理管理,避免内存泄漏。
2.7 前置工具准备(确保开发顺畅)
- 测试设备:鸿蒙手机(Mate 80 Pro Max)、鸿蒙平板(MatePad Pro 11)、DAYU200 开发板(已刷入鸿蒙 API10 + 系统,开启 USB 调试);
- 图表素材:准备统计图表相关图标(折线图、柱状图、饼图切换图标),放入
assets/icons目录; - 头像素材:准备默认头像(未登录时显示),放入
assets/images目录; - 原子化服务图标:准备原子化服务专用图标(尺寸符合鸿蒙规范:192x192),放入
entry/src/main/media目录; - 调试工具:开启 Flutter DevTools 的性能监控、布局检查功能,实时监控 APP 性能、调试布局问题;开启鸿蒙原子化服务调试模式,便于调试原子化服务启动、跳转问题。
三、基础准备:新增常量与工具类(支撑全模块开发)
在开发核心功能前,先新增用户中心、数据统计、原子化服务相关的常量与工具类,统一管理配置、封装通用方法,避免硬编码,减少重复开发,确保后续开发高效顺畅。
3.1 用户中心相关常量(统一配置)
文件路径:lib/core/constants/user_constants.dart
dart
// 用户中心全局常量(统一配置,便于修改)
class UserConstants {
// 用户默认头像(未登录/未设置头像时显示)
static const String defaultAvatar = "assets/images/ic_default_avatar.png";
// 头像存储路径(本地私有目录)
static const String avatarStoragePath = "smart_home/avatar";
// 手机号校验正则(中国大陆手机号)
static const String phoneRegex = r'^1[3-9]\d{9}$';
// 密码校验正则(8-16位,包含字母+数字)
static const String passwordRegex = r'^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z\d]{8,16}$';
// 验证码长度
static const int verificationCodeLength = 6;
// 验证码有效时间(60秒)
static const int verificationCodeExpireTime = 60;
// 用户登录状态缓存key(shared_preferences)
static const String userLoginStatusKey = "user_login_status";
// 当前登录用户ID缓存key
static const String currentUserIdKey = "current_user_id";
// 用户信息缓存key(ObjectBox查询)
static const String userInfoCacheKey = "current_user_info";
// 密码加密密钥(自定义,可修改,确保安全性)
static const String passwordEncryptKey = "smart_home_user_password_key_2024";
// 用户性别选项
static const List<Map<String, String>> genderOptions = [
{"value": "male", "name": "男"},
{"value": "female", "name": "女"},
{"value": "other", "name": "其他"},
];
// 登录方式
static const List<Map<String, String>> loginTypeOptions = [
{"value": "phone", "name": "手机号登录"},
{"value": "password", "name": "密码登录"},
];
// 用户状态
static const String userStatusNormal = "normal"; // 正常
static const String userStatusForbidden = "forbidden"; // 禁用
// 个人中心菜单选项(图标+标题+路由)
static const List<Map<String, String>> personalCenterMenus = [
{
"icon": "assets/icons/ic_user_info.png",
"title": "个人信息",
"route": "/user/info",
},
{
"icon": "assets/icons/ic_change_password.png",
"title": "修改密码",
"route": "/user/change_password",
},
{
"icon": "assets/icons/ic_privacy.png",
"title": "隐私设置",
"route": "/user/privacy",
},
{
"icon": "assets/icons/ic_help.png",
"title": "帮助与反馈",
"route": "/user/help",
},
{
"icon": "assets/icons/ic_about.png",
"title": "关于我们",
"route": "/user/about",
},
];
// 头像选择选项
static const List<Map<String, String>> avatarSelectOptions = [
{"value": "camera", "name": "拍摄照片"},
{"value": "album", "name": "从相册选择"},
];
// 登录失败提示语
static const Map<String, String> loginFailTips = {
"phone_error": "手机号格式不正确",
"password_error": "密码格式不正确(8-16位,字母+数字)",
"code_error": "验证码不正确或已过期",
"user_not_exist": "该用户不存在,请先注册",
"password_wrong": "密码错误,请重新输入",
"network_error": "网络异常,请检查网络后重试",
"system_error": "系统异常,请稍后重试",
};
// 注册失败提示语
static const Map<String, String> registerFailTips = {
"phone_exist": "该手机号已注册,请直接登录",
"phone_error": "手机号格式不正确",
"password_error": "密码格式不正确(8-16位,字母+数字)",
"code_error": "验证码不正确或已过期",
"password_confirm_error": "两次输入的密码不一致",
"network_error": "网络异常,请检查网络后重试",
"system_error": "系统异常,请稍后重试",
};
}
3.2 数据统计相关常量(统一配置)
文件路径:lib/core/constants/statistics_constants.dart
dart
// 数据统计全局常量(统一配置,便于修改)
class StatisticsConstants {
// 统计时间筛选选项
static const List<Map<String, String>> timeFilterOptions = [
{"value": "today", "name": "今日"},
{"value": "this_week", "name": "本周"},
{"value": "this_month", "name": "本月"},
{"value": "custom", "name": "自定义"},
];
// 统计维度选项(图标+标题+维度key)
static const List<Map<String, String>> statisticsDimensionOptions = [
{
"icon": "assets/icons/ic_device_statistics.png",
"title": "设备使用统计",
"key": "device",
},
{
"icon": "assets/icons/ic_scene_statistics.png",
"title": "场景执行统计",
"key": "scene",
},
{
"icon": "assets/icons/ic_timer_statistics.png",
"title": "定时触发统计",
"key": "timer",
},
{
"icon": "assets/icons/ic_alert_statistics.png",
"title": "异常发生统计",
"key": "alert",
},
];
// 设备使用统计指标
static const List<Map<String, String>> deviceStatisticsIndicators = [
{"value": "use_duration", "name": "使用时长(小时)"},
{"value": "control_count", "name": "控制次数"},
{"value": "online_rate", "name": "在线率(%)"},
];
// 图表类型选项
static const List<Map<String, String>> chartTypeOptions = [
{"value": "line", "name": "折线图"},
{"value": "bar", "name": "柱状图"},
{"value": "pie", "name": "饼图"},
];
// 统计数据缓存时间(30分钟,避免频繁计算)
static const int statisticsCacheTime = 30 * 60 * 1000;
// 设备使用时长单位(小时)
static const String deviceUseDurationUnit = "h";
// 控制次数/执行次数单位(次)
static const String countUnit = "次";
// 在线率单位(%)
static const String rateUnit = "%";
// 统计图表颜色(适配多端,区分不同设备/场景)
static const List<Color> chartColors = [
Color(0xFF2196F3),
Color(0xFF4CAF50),
Color(0xFFFF9800),
Color(0xFFF44336),
Color(0xFF9C27B0),
Color(0xFF00BCD4),
Color(0xFF795548),
Color(0xFF607D8B),
];
// 统计空数据提示语
static const Map<String, String> emptyDataTips = {
"device": "暂无设备使用数据,请先控制设备",
"scene": "暂无场景执行数据,请先执行场景",
"timer": "暂无定时触发数据,请先创建定时任务",
"alert": "暂无异常发生数据,设备运行正常",
};
// 统计数据计算失败提示语
static const String calculateFailTip = "统计数据计算失败,请重试";
// 图表加载失败提示语
static const String chartLoadFailTip = "图表加载失败,请重试";
}
3.3 鸿蒙原子化服务相关常量(统一配置)
文件路径:lib/core/constants/atom_service_constants.dart
dart
// 鸿蒙原子化服务全局常量(统一配置,便于修改)
class AtomServiceConstants {
// 原子化服务包名(与主APP包名区分,格式:主包名.atom)
static const String atomServicePackageName = "com.example.smart_home.atom";
// 主APP包名(与module.json5中一致)
static const String mainAppPackageName = "com.example.smart_home";
// 原子化服务主页面路由
static const String atomServiceMainRoute = "/atom_service/main";
// 原子化服务快捷设备控制页面路由
static const String atomServiceDeviceControlRoute = "/atom_service/device_control";
// 原子化服务场景执行页面路由
static const String atomServiceSceneExecuteRoute = "/atom_service/scene_execute";
// 原子化服务跳转主APP的Action
static const String jumpToMainAppAction = "ohos.intent.action.START_MAIN_APP";
// 原子化服务数据共享key(设备数据)
static const String atomServiceDeviceDataKey = "atom_service_device_data";
// 原子化服务数据共享key(场景数据)
static const String atomServiceSceneDataKey = "atom_service_scene_data";
// 原子化服务启动失败提示语
static const Map<String, String> startFailTips = {
"permission_error": "原子化服务权限不足,请开启相关权限",
"version_error": "原子化服务版本过低,请更新",
"system_error": "系统不支持原子化服务,请升级鸿蒙系统",
"network_error": "网络异常,无法加载数据",
"load_data_fail": "数据加载失败,请重试",
};
// 原子化服务快捷设备卡片数量(适配多端)
static int get deviceCardCount {
// 后续通过适配工具类判断设备类型,返回对应数量
// 手机:2个/行,平板:3个/行,开发板:2个/行
return 2;
}
// 原子化服务场景快捷按钮数量
static const int sceneButtonCount = 3;
// 原子化服务页面间距(适配多端)
static double get pagePadding {
// 后续通过适配工具类完善
return 16.0;
}
// 原子化服务卡片高度(适配多端)
static double get cardHeight {
return 120.0;
}
}
3.4 用户中心工具类(核心:密码加密、头像处理、验证码)
封装用户登录 / 注册过程中的通用方法,包括密码加密、手机号 / 密码 / 验证码校验、头像选择 / 拍摄 / 存储,提升开发效率,确保安全性和统一性。
文件路径:lib/core/user/user_tool.dart
dart
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:image_picker/image_picker.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:logger/logger.dart';
import 'package:smart_home_flutter/core/constants/user_constants.dart';
final Logger _logger = Logger();
final ImagePicker _imagePicker = ImagePicker();
// 用户中心工具类(单例模式)
class UserTool {
static UserTool? _instance;
static UserTool get instance => _instance ??= UserTool._internal();
UserTool._internal();
// 手机号校验
bool validatePhone(String phone) {
if (phone.isEmpty) return false;
return RegExp(UserConstants.phoneRegex).hasMatch(phone);
}
// 密码校验
bool validatePassword(String password) {
if (password.isEmpty) return false;
return RegExp(UserConstants.passwordRegex).hasMatch(password);
}
// 验证码校验(长度+非空)
bool validateVerificationCode(String code) {
if (code.isEmpty) return false;
return code.length == UserConstants.verificationCodeLength;
}
// 密码加密(使用encrypt插件,避免明文存储)
String encryptPassword(String password) {
try {
// 密钥(长度必须为16/24/32位,与UserConstants中一致)
final key = encrypt.Key.fromUtf8(UserConstants.passwordEncryptKey.padRight(32, '0').substring(0, 32));
// 偏移量(长度必须为16位)
final iv = encrypt.IV.fromUtf8('smart_home_iv_2024');
// 加密算法(AES)
final encrypter = encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc));
// 加密(补全长度)
final encrypted = encrypter.encryptPad(password, iv: iv);
// 返回加密后的字符串(base64编码)
return encrypted.base64;
} catch (e) {
_logger.e("密码加密失败:$e");
return password; // 加密失败时返回原密码(兜底,实际开发中需处理)
}
}
// 密码解密(用于密码验证、密码修改)
String decryptPassword(String encryptedPassword) {
try {
final key = encrypt.Key.fromUtf8(UserConstants.passwordEncryptKey.padRight(32, '0').substring(0, 32));
final iv = encrypt.IV.fromUtf8('smart_home_iv_2024');
final encrypter = encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc));
// 解密
final decrypted = encrypter.decryptPad(encrypt.Encrypted.fromBase64(encryptedPassword), iv: iv);
return decrypted;
} catch (e) {
_logger.e("密码解密失败:$e");
return "";
}
}
// 生成随机验证码(6位数字)
String generateVerificationCode() {
return (100000 + Random().nextInt(900000)).toString();
}
// 选择/拍摄头像
Future<File?> pickAvatar({required String sourceType}) async {
try {
XFile? pickedFile;
if (sourceType == "camera") {
// 拍摄照片
pickedFile = await _imagePicker.pickImage(
source: ImageSource.camera,
imageQuality: 80, // 图片质量(0-100)
preferredCameraDevice: CameraDevice.front, // 优先前置摄像头
);
} else if (sourceType == "album") {
// 从相册选择
pickedFile = await _imagePicker.pickImage(
source: ImageSource.gallery,
imageQuality: 80,
);
}
if (pickedFile == null) return null;
// 将XFile转为File
File avatarFile = File(pickedFile.path);
// 保存头像到本地私有目录(避免被系统清理)
return await saveAvatarToLocal(avatarFile);
} catch (e) {
_logger.e("头像选择/拍摄失败:$e");
return null;
}
}
// 保存头像到本地私有目录
Future<File> saveAvatarToLocal(File avatarFile) async {
try {
// 获取应用私有存储目录
final directory = await getApplicationDocumentsDirectory();
// 创建头像存储目录(不存在则创建)
final avatarDirectory = Directory("${directory.path}/${UserConstants.avatarStoragePath}");
if (!await avatarDirectory.exists()) {
await avatarDirectory.create(recursive: true);
}
// 生成头像文件名(时间戳+原文件名后缀,避免重复)
final fileName = "${DateTime.now().millisecondsSinceEpoch}${path.extension(avatarFile.path)}";
// 头像存储路径
final avatarPath = path.join(avatarDirectory.path, fileName);
// 复制文件到私有目录
final savedAvatarFile = await avatarFile.copy(avatarPath);
_logger.d("头像保存成功,路径:$avatarPath");
return savedAvatarFile;
} catch (e) {
_logger.e("头像保存失败:$e");
rethrow;
}
}
// 获取本地头像文件(根据路径)
Future<File?> getLocalAvatarFile(String avatarPath) async {
try {
final file = File(avatarPath);
if (await file.exists()) {
return file;
}
_logger.w("本地头像文件不存在:$avatarPath");
return null;
} catch (e) {
_logger.e("获取本地头像失败:$e");
return null;
}
}
// 删除本地头像文件
Future<bool> deleteLocalAvatarFile(String avatarPath) async {
try {
final file = File(avatarPath);
if (await file.exists()) {
await file.delete();
_logger.d("本地头像文件删除成功:$avatarPath");
return true;
}
return false;
} catch (e) {
_logger.e("本地头像文件删除失败:$e");
return false;
}
}
// 格式化用户性别(用于UI展示)
String formatGender(String gender) {
for (var option in UserConstants.genderOptions) {
if (option["value"] == gender) {
return option["name"] ?? gender;
}
}
return "未知";
}
// 检查登录状态(从shared_preferences获取)
Future<bool> checkLoginStatus() async {
try {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(UserConstants.userLoginStatusKey) ?? false;
} catch (e) {
_logger.e("检查登录状态失败:$e");
return false;
}
}
// 保存登录状态(到shared_preferences)
Future<void> saveLoginStatus(bool isLogin) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(UserConstants.userLoginStatusKey, isLogin);
_logger.d("登录状态保存成功:$isLogin");
} catch (e) {
_logger.e("登录状态保存失败:$e");
}
}
// 保存当前登录用户ID(到shared_preferences)
Future<void> saveCurrentUserId(String userId) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(UserConstants.currentUserIdKey, userId);
_logger.d("当前登录用户ID保存成功:$userId");
} catch (e) {
_logger.e("当前登录用户ID保存失败:$e");
}
}
// 获取当前登录用户ID(从shared_preferences获取)
Future<String?> getCurrentUserId() async {
try {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(UserConstants.currentUserIdKey);
} catch (e) {
_logger.e("获取当前登录用户ID失败:$e");
return null;
}
}
// 清除登录相关缓存(退出登录时调用)
Future<void> clearLoginCache() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(UserConstants.userLoginStatusKey);
await prefs.remove(UserConstants.currentUserIdKey);
_logger.d("登录相关缓存清除成功");
} catch (e) {
_logger.e("登录相关缓存清除失败:$e");
}
}
}
3.5 数据统计工具类(核心:数据计算、日期处理)
封装数据统计过程中的通用方法,包括统计数据计算(设备使用时长、场景执行次数等)、日期格式化、时间范围筛选,避免重复计算,确保统计数据准确。
文件路径:lib/core/statistics/statistics_tool.dart
dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:logger/logger.dart';
import 'package:smart_home_flutter/core/constants/statistics_constants.dart';
import 'package:smart_home_flutter/data/models/device_operation_log_model.dart';
import 'package:smart_home_flutter/data/models/smart_scene_model.dart';
import 'package:smart_home_flutter/data/models/timer_task_model.dart';
import 'package:smart_home_flutter/data/models/device_alert_record_model.dart';
final Logger _logger = Logger();
// 数据统计工具类(单例模式)
class StatisticsTool {
static StatisticsTool? _instance;
static StatisticsTool get instance => _instance ??= StatisticsTool._internal();
StatisticsTool._internal();
// 日期格式化工具(统一格式)
final DateFormat _dateFormat = DateFormat("yyyy-MM-dd");
final DateFormat _dateTimeFormat = DateFormat("yyyy-MM-dd HH:mm:ss");
final DateFormat _timeFormat = DateFormat("HH:mm");
// 格式化日期(yyyy-MM-dd)
String formatDate(DateTime date) {
return _dateFormat.format(date);
}
// 格式化日期时间(yyyy-MM-dd HH:mm:ss)
String formatDateTime(DateTime date) {
return _dateTimeFormat.format(date);
}
// 格式化时间(HH:mm)
String formatTime(DateTime date) {
return _timeFormat.format(date);
}
// 根据时间筛选条件,获取时间范围(开始时间+结束时间)
Map<String, DateTime> getTimeRange(String timeFilter) {
final now = DateTime.now();
late DateTime startTime;
late DateTime endTime;
switch (timeFilter) {
case "today":
// 今日:00:00:00 到 当前时间
startTime = DateTime(now.year, now.month, now.day, 0, 0, 0);
endTime = now;
break;
case "this_week":
// 本周:周一00:00:00 到 当前时间(鸿蒙系统周一为一周第一天)
final weekDay = now.weekday; // 1=周一,7=周日
final daysToMonday = weekDay - 1;
startTime = DateTime(now.year, now.month, now.day - daysToMonday, 0, 0, 0);
endTime = now;
break;
case "this_month":
// 本月:1号00:00:00 到 当前时间
startTime = DateTime(now.year, now.month, 1, 0, 0, 0);
endTime = now;
break;
case "custom":
// 自定义:默认近7天(后续在UI层让用户选择)
startTime = now.subtract(const Duration(days: 7));
endTime = now;
break;
default:
// 默认今日
startTime = DateTime(now.year, now.month, now.day, 0, 0, 0);
endTime = now;
}
_logger.d("时间筛选:$timeFilter,时间范围:${formatDateTime(startTime)} 到 ${formatDateTime(endTime)}");
return {"startTime": startTime, "endTime": endTime};
}
// 计算设备使用统计数据(使用时长、控制次数、在线率)
Map<String, Map<String, dynamic>> calculateDeviceStatistics({
required List<DeviceOperationLog> operationLogs,
required List<BaseDevice> devices,
required String timeFilter,
}) {
try {
final timeRange = getTimeRange(timeFilter);
final startTime = timeRange["startTime"]!;
final endTime = timeRange["endTime"]!;
// 按设备ID分组统计
final Map<String, Map<String, dynamic>> deviceStats = {};
// 初始化所有设备的统计数据
for (var device in devices) {
deviceStats[device.deviceId] = {
"deviceId": device.deviceId,
"deviceName": device.name,
"deviceType": device.type.name,
"useDuration": 0.0, // 使用时长(小时)
"controlCount": 0, // 控制次数
"onlineRate": 0.0, // 在线率(%)
"onlineTime": 0, // 在线时间(毫秒)
"totalTime": endTime.difference(startTime).inMilliseconds, // 统计总时长(毫秒)
};
}
// 统计控制次数和使用时长(根据操作日志)
for (var log in operationLogs) {
final logTime = DateTime.fromMillisecondsSinceEpoch(log.operationTime);
// 筛选当前时间范围内的日志
if (logTime.isAfter(startTime) && logTime.isBefore(endTime)) {
final deviceId = log.deviceId;
if (deviceStats.containsKey(deviceId)) {
// 控制次数+1
deviceStats[deviceId]!["controlCount"] = deviceStats[deviceId]!["controlCount"] + 1;
// 计算使用时长(仅统计“开启设备”到“关闭设备”的时间差)
if (log.operationType == "turn_on") {
// 开启设备:记录开启时间
deviceStats[deviceId]!["turnOnTime"] = log.operationTime;
} else if (log.operationType == "turn_off" && deviceStats[deviceId]!.containsKey("turnOnTime")) {
// 关闭设备:计算开启到关闭的时间差
final turnOnTime = deviceStats[deviceId]!["turnOnTime"];
final duration = log.operationTime - turnOnTime;
// 累加使用时长(转换为小时)
deviceStats[deviceId]!["useDuration"] = deviceStats[deviceId]!["useDuration"] + (duration / (1000 * 60 * 60));
// 移除开启时间标记
deviceStats[deviceId]!.remove("turnOnTime");
}
}
}
}
// 计算在线率(简化计算:假设设备在线状态可通过最近一次操作日志判断)
for (var deviceId in deviceStats.keys) {
final device = devices.firstWhere((e) => e.deviceId == deviceId);
// 简化逻辑:设备当前在线,则在线率=(在线时间/统计总时长)*100,此处假设在线时间=统计总时长的80%(实际开发中需对接设备在线状态接口)
if (device.isOnline) {
deviceStats[deviceId]!["onlineRate"] = 80.0; // 模拟数据,实际需替换为真实在线时间计算
} else {
deviceStats[deviceId]!["onlineRate"] = 0.0;
}
// 保留两位小数
deviceStats[deviceId]!["useDuration"] = double.parse(deviceStats[deviceId]!["useDuration"].toStringAsFixed(2));
deviceStats[deviceId]!["onlineRate"] = double.parse(deviceStats[deviceId]!["onlineRate"].toStringAsFixed(2));
}
_logger.d("设备使用统计计算完成,统计设备数量:${deviceStats.length}");
return deviceStats;
} catch (e) {
_logger.e("设备使用统计计算失败:$e");
return {};
}
}
// 计算场景执行统计数据(执行次数、成功次数、失败次数、成功率)
Map<String, Map<String, dynamic>> calculateSceneStatistics({
required List<SmartScene> scenes,
required List<DeviceOperationLog> operationLogs,
required String timeFilter,
}) {
try {
final timeRange = getTimeRange(timeFilter);
final startTime = timeRange["startTime"]!;
final endTime = timeRange["endTime"]!;
// 按场景ID分组统计
final Map<String, Map<String, dynamic>> sceneStats = {};
// 初始化所有场景的统计数据
for (var scene in scenes) {
sceneStats[scene.sceneId] = {
"sceneId": scene.sceneId,
"sceneName": scene.sceneName,
"executeCount": 0, // 执行次数
"successCount": 0, // 成功次数
"failCount": 0, // 失败次数
"successRate": 0.0, // 成功率(%)
};
}
// 统计场景执行次数、成功次数、失败次数(根据操作日志,触发来源为scene)
for (var log in operationLogs) {
final logTime = DateTime.fromMillisecondsSinceEpoch(log.operationTime);
if (logTime.isAfter(startTime) && logTime.isBefore(endTime) && log.triggerSource == "scene") {
final sceneId = log.triggerSourceId;
if (sceneStats.containsKey(sceneId)) {
// 执行次数+1
sceneStats[sceneId]!["executeCount"] = sceneStats[sceneId]!["executeCount"] + 1;
// 统计成功/失败次数
if (log.operationResult == "success") {
sceneStats[sceneId]!["successCount"] = sceneStats[sceneId]!["successCount"] + 1;
} else {
sceneStats[sceneId]!["failCount"] = sceneStats[sceneId]!["failCount"] + 1;
}
}
}
}
// 计算成功率
for (var sceneId in sceneStats.keys) {
final executeCount = sceneStats[sceneId]!["executeCount"];
final successCount = sceneStats[sceneId]!["successCount"];
if (executeCount > 0) {
sceneStats[sceneId]!["successRate"] = double.parse(((successCount / executeCount) * 100).toStringAsFixed(2));
} else {
sceneStats[sceneId]

所有评论(0)