From 34dbfd7fd7341f64247885631c799d74574e42b7 Mon Sep 17 00:00:00 2001
From: zyc <1439655764@qq.com>
Date: Mon, 9 Feb 2026 15:36:18 +0800
Subject: [PATCH 1/5] add setting
---
.claude/settings.local.json | 11 +
docs/API_SPECIFICATION.md | 1202 +++++++++++++++++++++++++++++++++++
2 files changed, 1213 insertions(+)
create mode 100644 .claude/settings.local.json
create mode 100644 docs/API_SPECIFICATION.md
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..c4ad5e2
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,11 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(flutter doctor:*)",
+ "Bash(flutter devices:*)",
+ "Bash(flutter pub get:*)",
+ "Bash(pod install)",
+ "Bash(flutter run:*)"
+ ]
+ }
+}
diff --git a/docs/API_SPECIFICATION.md b/docs/API_SPECIFICATION.md
new file mode 100644
index 0000000..2705c6e
--- /dev/null
+++ b/docs/API_SPECIFICATION.md
@@ -0,0 +1,1202 @@
+# Airhub 后台接口规范文档
+
+> 版本: 1.0.0
+> 更新日期: 2025-02-09
+> 基于 PRD HTML 原型与 airhub_app Flutter 代码分析整理
+
+---
+
+## 目录
+
+- [概述](#概述)
+- [通用规范](#通用规范)
+- [接口列表](#接口列表)
+ - [1. 用户认证](#1-用户认证)
+ - [2. 用户信息](#2-用户信息)
+ - [3. 设备管理](#3-设备管理)
+ - [4. 角色记忆](#4-角色记忆)
+ - [5. 故事模块](#5-故事模块)
+ - [6. 音乐模块](#6-音乐模块)
+ - [7. 通知模块](#7-通知模块)
+ - [8. 系统接口](#8-系统接口)
+- [数据模型](#数据模型)
+- [开发优先级](#开发优先级)
+- [附录:代码对照表](#附录代码对照表)
+
+---
+
+## 概述
+
+本文档定义了 Airhub 智能硬件控制中心 APP 所需的全部后台接口。接口设计基于以下来源:
+
+1. **PRD HTML 原型文件** - 根目录下的 `.html` 文件
+2. **airhub_app Flutter 代码** - `airhub_app/lib/` 目录
+3. **已实现的接口调用** - `music-creation.html` 中的实际 API 调用
+
+### 技术栈建议
+
+- 后端框架: Node.js (Express/Fastify) 或 Python (FastAPI)
+- 数据库: PostgreSQL / MySQL
+- 缓存: Redis
+- 对象存储: 阿里云 OSS
+- 实时通信: SSE (Server-Sent Events)
+
+---
+
+## 通用规范
+
+### 基础 URL
+
+```
+开发环境: http://localhost:3000/api
+生产环境: https://api.airhub.com/v1
+```
+
+### 请求头
+
+```http
+Content-Type: application/json
+Authorization: Bearer {access_token}
+X-Device-Id: {device_uuid}
+X-App-Version: 1.0.0
+```
+
+### 响应格式
+
+**成功响应**
+```json
+{
+ "code": 0,
+ "message": "success",
+ "data": { ... }
+}
+```
+
+**错误响应**
+```json
+{
+ "code": 40001,
+ "message": "参数错误",
+ "data": null
+}
+```
+
+### 错误码定义
+
+| 错误码 | 说明 |
+|--------|------|
+| 0 | 成功 |
+| 40001 | 参数错误 |
+| 40101 | 未授权 (Token 无效) |
+| 40102 | Token 过期 |
+| 40301 | 无权限 |
+| 40401 | 资源不存在 |
+| 50001 | 服务器内部错误 |
+
+---
+
+## 接口列表
+
+### 1. 用户认证
+
+#### 1.1 发送验证码
+
+```http
+POST /api/auth/send-code
+```
+
+**请求参数**
+```json
+{
+ "phone": "13800138000"
+}
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "message": "验证码已发送",
+ "data": {
+ "expire_in": 60
+ }
+}
+```
+
+**说明**: 验证码 60 秒内有效,同一手机号 60 秒内只能发送一次
+
+---
+
+#### 1.2 验证码登录
+
+```http
+POST /api/auth/login
+```
+
+**请求参数**
+```json
+{
+ "phone": "13800138000",
+ "code": "123456"
+}
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "message": "登录成功",
+ "data": {
+ "access_token": "eyJhbGciOiJIUzI1NiIs...",
+ "refresh_token": "eyJhbGciOiJIUzI1NiIs...",
+ "expire_in": 604800,
+ "user": {
+ "id": "u_12345678",
+ "phone": "138****8000",
+ "nickname": "用户8000",
+ "avatar_url": null
+ }
+ }
+}
+```
+
+**Flutter 代码对应**: `AuthRemoteDataSource.loginWithPhone()`
+
+---
+
+#### 1.3 一键登录
+
+```http
+POST /api/auth/one-click-login
+```
+
+**请求参数**
+```json
+{
+ "access_token": "运营商SDK返回的token"
+}
+```
+
+**响应**: 同验证码登录
+
+**说明**: 使用运营商 SDK (如阿里云号码认证) 获取本机号码
+
+**Flutter 代码对应**: `AuthRemoteDataSource.oneClickLogin()`
+
+---
+
+#### 1.4 刷新 Token
+
+```http
+POST /api/auth/refresh-token
+```
+
+**请求参数**
+```json
+{
+ "refresh_token": "eyJhbGciOiJIUzI1NiIs..."
+}
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "data": {
+ "access_token": "新的access_token",
+ "expire_in": 604800
+ }
+}
+```
+
+---
+
+#### 1.5 退出登录
+
+```http
+POST /api/auth/logout
+```
+
+**请求头**: 需要 Authorization
+
+**响应**
+```json
+{
+ "code": 0,
+ "message": "已退出登录"
+}
+```
+
+**Flutter 代码对应**: `AuthRepository.logout()`
+
+---
+
+#### 1.6 账号注销
+
+```http
+DELETE /api/auth/account
+```
+
+**请求头**: 需要 Authorization
+
+**响应**
+```json
+{
+ "code": 0,
+ "message": "账号注销申请已提交,将在7个工作日内处理"
+}
+```
+
+**PRD 来源**: `settings.html` - 账号注销功能
+
+---
+
+### 2. 用户信息
+
+#### 2.1 获取用户资料
+
+```http
+GET /api/user/profile
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "data": {
+ "id": "u_12345678",
+ "phone": "138****8000",
+ "nickname": "土豆",
+ "avatar_url": "https://oss.airhub.com/avatars/xxx.jpg",
+ "gender": "男",
+ "birthday": "1994-12-09",
+ "created_at": "2025-01-15T10:30:00Z"
+ }
+}
+```
+
+**Flutter 代码对应**: `profile_page.dart` - 用户卡片显示
+
+---
+
+#### 2.2 更新用户资料
+
+```http
+PUT /api/user/profile
+```
+
+**请求参数**
+```json
+{
+ "nickname": "新昵称",
+ "gender": "女",
+ "birthday": "1995-06-15"
+}
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "message": "保存成功"
+}
+```
+
+**Flutter 代码对应**: `profile_info_page.dart` - 保存按钮
+
+---
+
+#### 2.3 上传头像
+
+```http
+POST /api/user/avatar
+Content-Type: multipart/form-data
+```
+
+**请求参数**
+```
+file: (binary)
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "data": {
+ "avatar_url": "https://oss.airhub.com/avatars/xxx.jpg"
+ }
+}
+```
+
+**Flutter 代码对应**: `profile_info_page.dart` - `_pickImage()`
+
+---
+
+### 3. 设备管理
+
+#### 3.1 获取设备列表
+
+```http
+GET /api/devices
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "data": {
+ "devices": [
+ {
+ "id": "dev_001",
+ "name": "毛绒机芯",
+ "model": "Airhub_5G",
+ "status": "online",
+ "battery": 85,
+ "firmware_version": "2.1.3",
+ "is_ai": true,
+ "icon": "Capybara.png"
+ },
+ {
+ "id": "dev_002",
+ "name": "电子吧唧 AI",
+ "model": "Badge_AI",
+ "status": "offline",
+ "battery": 0,
+ "is_ai": true
+ }
+ ]
+ }
+}
+```
+
+**设备状态枚举**:
+- `online` - 在线
+- `offline` - 离线
+- `pairing` - 配对中
+- `updating` - 固件更新中
+
+**Flutter 代码对应**: `product_selection_page.dart`
+
+---
+
+#### 3.2 获取设备详情
+
+```http
+GET /api/devices/{device_id}
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "data": {
+ "id": "dev_001",
+ "name": "毛绒机芯",
+ "model": "Airhub_5G",
+ "status": "online",
+ "battery": 85,
+ "firmware_version": "2.1.3",
+ "mac_address": "AA:BB:CC:DD:EE:FF",
+ "settings": {
+ "nickname": "小毛球",
+ "user_name": "土豆",
+ "volume": 60,
+ "brightness": 85,
+ "allow_interrupt": true,
+ "privacy_mode": true
+ },
+ "wifi_list": [
+ {
+ "ssid": "Home_WiFi",
+ "is_connected": true
+ }
+ ],
+ "bound_memory_id": "mem_001"
+ }
+}
+```
+
+**Flutter 代码对应**: `device_control_page.dart` - 设置页面
+
+---
+
+#### 3.3 更新设备设置
+
+```http
+PUT /api/devices/{device_id}
+```
+
+**请求参数**
+```json
+{
+ "nickname": "新昵称",
+ "user_name": "主人称呼",
+ "volume": 70,
+ "brightness": 90,
+ "allow_interrupt": false,
+ "privacy_mode": true
+}
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "message": "设置已保存"
+}
+```
+
+**PRD 来源**: `device-control.html` - 设备设置面板
+
+---
+
+#### 3.4 绑定设备
+
+```http
+POST /api/devices/{device_id}/bind
+```
+
+**请求参数**
+```json
+{
+ "mac_address": "AA:BB:CC:DD:EE:FF",
+ "wifi_ssid": "Home_WiFi",
+ "wifi_password": "password123"
+}
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "message": "设备绑定成功",
+ "data": {
+ "device_id": "dev_001",
+ "memory_id": "mem_001"
+ }
+}
+```
+
+**PRD 来源**: `bluetooth.html`, `wifi-config.html`
+
+---
+
+#### 3.5 解绑设备
+
+```http
+DELETE /api/devices/{device_id}/unbind
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "message": "设备已解绑",
+ "data": {
+ "memory_id": "mem_001",
+ "memory_name": "Cloud_Mem_01"
+ }
+}
+```
+
+**说明**: 解绑后角色记忆自动保存到云端
+
+**PRD 来源**: `device-control.html` - 解绑设备弹窗
+
+---
+
+#### 3.6 配置设备 WiFi
+
+```http
+POST /api/devices/{device_id}/wifi
+```
+
+**请求参数**
+```json
+{
+ "ssid": "New_WiFi",
+ "password": "password123"
+}
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "message": "WiFi 配置成功"
+}
+```
+
+**PRD 来源**: `wifi-config.html`
+
+---
+
+### 4. 角色记忆
+
+#### 4.1 获取角色记忆列表
+
+```http
+GET /api/agents
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "data": {
+ "agents": [
+ {
+ "id": "mem_001",
+ "name": "Airhub_Mem_01",
+ "icon": "🧠",
+ "nickname": "小毛球",
+ "bound_device": {
+ "id": "dev_001",
+ "name": "Airhub_5G"
+ },
+ "status": "bound",
+ "created_at": "2025-01-15"
+ },
+ {
+ "id": "mem_002",
+ "name": "Airhub_Mem_02",
+ "icon": "🐾",
+ "nickname": "豆豆",
+ "bound_device": null,
+ "status": "unbound",
+ "created_at": "2024-08-22"
+ }
+ ]
+ }
+}
+```
+
+**角色记忆状态**:
+- `bound` - 已绑定设备
+- `unbound` - 未绑定 (可注入)
+
+**Flutter 代码对应**: `agent_manage_page.dart`
+
+---
+
+#### 4.2 解绑角色记忆
+
+```http
+POST /api/agents/{agent_id}/unbind
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "message": "已解绑角色记忆,数据已保留在云端"
+}
+```
+
+---
+
+#### 4.3 注入角色记忆
+
+```http
+POST /api/agents/{agent_id}/inject
+```
+
+**请求参数**
+```json
+{
+ "device_id": "dev_002"
+}
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "message": "角色记忆注入成功"
+}
+```
+
+**PRD 来源**: `agent-manage.html` - 注入设备按钮
+
+---
+
+### 5. 故事模块
+
+#### 5.1 获取故事列表
+
+```http
+GET /api/stories
+```
+
+**查询参数**
+```
+shelf_id: 1 # 书架 ID,可选
+page: 1 # 页码
+page_size: 20 # 每页数量
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "data": {
+ "stories": [
+ {
+ "id": "story_001",
+ "title": "卡皮巴拉的奇幻漂流",
+ "cover_url": "https://oss.airhub.com/covers/xxx.jpg",
+ "content": "在一条蜿蜒的小河边...",
+ "has_video": true,
+ "video_url": "https://oss.airhub.com/videos/xxx.mp4",
+ "created_at": "2025-01-20T15:30:00Z"
+ }
+ ],
+ "total": 5,
+ "page": 1,
+ "page_size": 20
+ }
+}
+```
+
+**Flutter 代码对应**: `device_control_page.dart` - `_mockStories`
+
+---
+
+#### 5.2 获取故事详情
+
+```http
+GET /api/stories/{story_id}
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "data": {
+ "id": "story_001",
+ "title": "卡皮巴拉的奇幻漂流",
+ "cover_url": "https://oss.airhub.com/covers/xxx.jpg",
+ "content": "完整故事内容...",
+ "has_video": true,
+ "video_url": "https://oss.airhub.com/videos/xxx.mp4",
+ "created_at": "2025-01-20T15:30:00Z"
+ }
+}
+```
+
+**Flutter 代码对应**: `story_detail_page.dart`
+
+---
+
+#### 5.3 生成故事 (SSE 流式)
+
+```http
+POST /api/stories/generate
+Accept: text/event-stream
+```
+
+**请求参数**
+```json
+{
+ "mode": "random",
+ "prompt": "关于卡皮巴拉的冒险故事",
+ "theme": "adventure"
+}
+```
+
+**生成模式**:
+- `random` - 随机生成
+- `keyword` - 关键词生成
+- `theme` - 主题生成
+
+**SSE 响应流**
+```
+data: {"stage": "generating", "progress": 10, "message": "正在构思故事大纲..."}
+
+data: {"stage": "generating", "progress": 50, "message": "正在撰写故事内容..."}
+
+data: {"stage": "done", "progress": 100, "story": {...}}
+```
+
+**Flutter 代码对应**: `story_loading_page.dart`
+
+---
+
+#### 5.4 保存故事
+
+```http
+POST /api/stories/{story_id}/save
+```
+
+**请求参数**
+```json
+{
+ "shelf_id": 1
+}
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "message": "故事已保存到书架"
+}
+```
+
+**PRD 来源**: `story-detail.html` - 保存故事按钮
+
+---
+
+#### 5.5 生成动态绘本 (SSE 流式)
+
+```http
+POST /api/stories/{story_id}/video
+Accept: text/event-stream
+```
+
+**SSE 响应流**
+```
+data: {"stage": "processing", "progress": 20, "message": "正在生成插画..."}
+
+data: {"stage": "processing", "progress": 60, "message": "正在合成视频..."}
+
+data: {"stage": "done", "progress": 100, "video_url": "https://..."}
+```
+
+**说明**: 生成动态绘本消耗 10 SP (积分)
+
+**Flutter 代码对应**: `story_detail_page.dart` - `runGenerationProcess()`
+
+---
+
+#### 5.6 解锁新书架
+
+```http
+POST /api/stories/shelves/unlock
+```
+
+**请求参数**
+```json
+{
+ "cost_points": 500
+}
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "message": "解锁成功",
+ "data": {
+ "shelf_id": 2,
+ "remaining_points": 1500
+ }
+}
+```
+
+**Flutter 代码对应**: `device_control_page.dart` - `_showUnlockDialog()`
+
+---
+
+### 6. 音乐模块
+
+> ⚠️ **重要**: 此模块在 `music-creation.html` 中已有实际 API 调用实现
+
+#### 6.1 生成音乐 (SSE 流式)
+
+```http
+POST /api/create_music
+Accept: text/event-stream
+```
+
+**请求参数**
+```json
+{
+ "text": "水豚在雨中等公交,心情却很平静",
+ "mood": "chill"
+}
+```
+
+**心情类型**:
+- `chill` - Chill Lofi (慵懒·治愈)
+- `happy` - Happy Funk (活力·奔跑)
+- `sleep` - Deep Sleep (白噪音·助眠)
+- `focus` - Focus Flow (心流·专注)
+- `mystery` - 盲盒惊喜 (AI随机)
+- `custom` - 自由创作
+
+**SSE 响应流**
+```
+data: {"stage": "connecting", "progress": 5, "message": "🎼 正在连接 AI..."}
+
+data: {"stage": "lyrics", "progress": 20, "message": "🎵 正在生成歌词..."}
+
+data: {"stage": "music", "progress": 30, "message": "🎹 正在作曲..."}
+
+data: {"stage": "done", "progress": 100, "file_path": "/music/xxx.mp3", "metadata": {"lyrics": "歌词内容..."}}
+```
+
+**错误响应**
+```
+data: {"stage": "error", "message": "生成失败,请重试"}
+```
+
+**HTML 代码位置**: `music-creation.html:1730`
+
+**Flutter 代码对应**: `music_creation_page.dart` - `_mockGenerate()`
+
+---
+
+#### 6.2 获取播放列表
+
+```http
+GET /api/playlist
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "data": {
+ "playlist": [
+ {
+ "id": 1,
+ "title": "卡皮巴拉蹦蹦蹦",
+ "lyrics": "卡皮巴拉\n啦啦啦啦...",
+ "audio_url": "/music/卡皮巴拉蹦蹦蹦.mp3",
+ "cover_url": "Capybara.png",
+ "mood": "happy",
+ "duration": 204,
+ "created_at": "2025-01-15T10:00:00Z"
+ }
+ ]
+ }
+}
+```
+
+**HTML 代码位置**: `music-creation.html:2006`
+
+**Flutter 代码对应**: `music_creation_page.dart` - `_playlist`
+
+---
+
+#### 6.3 删除播放列表项
+
+```http
+DELETE /api/playlist/{track_id}
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "message": "已从播放列表移除"
+}
+```
+
+---
+
+#### 6.4 收藏音乐
+
+```http
+POST /api/playlist/{track_id}/favorite
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "message": "已收藏"
+}
+```
+
+---
+
+### 7. 通知模块
+
+#### 7.1 获取通知列表
+
+```http
+GET /api/notifications
+```
+
+**查询参数**
+```
+type: all # all, system, activity, device
+page: 1
+page_size: 20
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "data": {
+ "notifications": [
+ {
+ "id": "notif_001",
+ "type": "system",
+ "title": "系统更新",
+ "description": "Airhub V1.2.0 版本更新已准备就绪",
+ "content": "
更新说明:
",
+ "is_read": false,
+ "created_at": "2025-02-09T10:30:00Z"
+ },
+ {
+ "id": "notif_002",
+ "type": "activity",
+ "title": "新春活动",
+ "description": "领取您的新春限定水豚皮肤",
+ "image_url": "https://...",
+ "is_read": true,
+ "created_at": "2025-02-08T09:00:00Z"
+ }
+ ],
+ "unread_count": 1
+ }
+}
+```
+
+**通知类型**:
+- `system` - 系统通知
+- `activity` - 活动通知
+- `device` - 设备通知
+
+**Flutter 代码对应**: `notification_page.dart`
+
+---
+
+#### 7.2 标记通知已读
+
+```http
+PUT /api/notifications/{notification_id}/read
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "message": "已标记为已读"
+}
+```
+
+---
+
+#### 7.3 标记全部已读
+
+```http
+PUT /api/notifications/read-all
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "message": "全部标记为已读"
+}
+```
+
+---
+
+### 8. 系统接口
+
+#### 8.1 提交意见反馈
+
+```http
+POST /api/feedback
+```
+
+**请求参数**
+```json
+{
+ "content": "希望增加深色模式功能",
+ "contact": "user@email.com"
+}
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "message": "感谢您的反馈!"
+}
+```
+
+**Flutter 代码对应**: `feedback_dialog.dart`
+
+---
+
+#### 8.2 检查版本更新
+
+```http
+GET /api/version/check
+```
+
+**查询参数**
+```
+platform: ios # ios, android
+current_version: 1.0.0
+```
+
+**响应**
+```json
+{
+ "code": 0,
+ "data": {
+ "latest_version": "1.2.0",
+ "current_version": "1.0.0",
+ "update_required": false,
+ "force_update": false,
+ "update_url": "https://apps.apple.com/...",
+ "release_notes": "1. 新增喂养指南\n2. 优化连接稳定性"
+ }
+}
+```
+
+**Flutter 代码对应**: `settings_page.dart` - 检查更新
+
+---
+
+## 数据模型
+
+### User (用户)
+
+```typescript
+interface User {
+ id: string;
+ phone: string; // 脱敏显示
+ nickname: string;
+ avatar_url: string | null;
+ gender: '男' | '女' | null;
+ birthday: string | null; // YYYY-MM-DD
+ created_at: string;
+}
+```
+
+### Device (设备)
+
+```typescript
+interface Device {
+ id: string;
+ name: string;
+ model: string;
+ status: 'online' | 'offline' | 'pairing' | 'updating';
+ battery: number; // 0-100
+ firmware_version: string;
+ mac_address: string;
+ is_ai: boolean;
+ settings: DeviceSettings;
+ bound_memory_id: string | null;
+}
+
+interface DeviceSettings {
+ nickname: string; // 设备昵称
+ user_name: string; // 用户称呼
+ volume: number; // 0-100
+ brightness: number; // 0-100
+ allow_interrupt: boolean;
+ privacy_mode: boolean;
+}
+```
+
+### Agent (角色记忆)
+
+```typescript
+interface Agent {
+ id: string;
+ name: string; // Airhub_Mem_01
+ icon: string; // Emoji
+ nickname: string; // 小毛球
+ bound_device: Device | null;
+ status: 'bound' | 'unbound';
+ created_at: string;
+}
+```
+
+### Story (故事)
+
+```typescript
+interface Story {
+ id: string;
+ title: string;
+ cover_url: string;
+ content: string;
+ has_video: boolean;
+ video_url: string | null;
+ shelf_id: number;
+ created_at: string;
+}
+```
+
+### Track (音乐)
+
+```typescript
+interface Track {
+ id: number;
+ title: string;
+ lyrics: string;
+ audio_url: string;
+ cover_url: string;
+ mood: 'chill' | 'happy' | 'sleep' | 'focus' | 'mystery' | 'custom';
+ duration: number; // 秒
+ created_at: string;
+}
+```
+
+### Notification (通知)
+
+```typescript
+interface Notification {
+ id: string;
+ type: 'system' | 'activity' | 'device';
+ title: string;
+ description: string;
+ content: string | null; // HTML 内容
+ image_url: string | null;
+ is_read: boolean;
+ created_at: string;
+}
+```
+
+---
+
+## 开发优先级
+
+| 优先级 | 模块 | 说明 | 依赖 |
+|--------|------|------|------|
+| **P0** | 用户认证 | 登录/注册是基础功能 | 运营商 SDK |
+| **P0** | 设备管理 | 核心业务:设备绑定、配网 | 蓝牙 SDK |
+| **P1** | 音乐模块 | 已有 HTML 实现,需对接 MiniMax API | MiniMax API |
+| **P1** | 故事模块 | 核心功能:AI 生成故事 | LLM API |
+| **P2** | 角色记忆 | 差异化功能 | 设备管理 |
+| **P2** | 用户信息 | 个人中心相关 | 用户认证 |
+| **P3** | 通知模块 | 辅助功能 | 推送 SDK |
+| **P3** | 系统接口 | 反馈、版本检查 | - |
+
+---
+
+## 附录:代码对照表
+
+| 接口 | PRD HTML 文件 | Flutter 代码 |
+|------|---------------|--------------|
+| 登录 | `login.html` | `auth_remote_data_source.dart` |
+| 用户资料 | `profile.html`, `profile-info.html` | `profile_page.dart`, `profile_info_page.dart` |
+| 设备管理 | `products.html`, `device-control.html` | `product_selection_page.dart`, `device_control_page.dart` |
+| 配网 | `bluetooth.html`, `wifi-config.html` | `bluetooth_page.dart`, `wifi_config_page.dart` |
+| 角色记忆 | `agent-manage.html` | `agent_manage_page.dart` |
+| 故事 | `story-detail.html`, `story-loading.html` | `story_detail_page.dart`, `story_loading_page.dart` |
+| 音乐 | `music-creation.html` | `music_creation_page.dart` |
+| 通知 | `notifications.html` | `notification_page.dart` |
+| 设置 | `settings.html` | `settings_page.dart` |
+
+---
+
+## 更新日志
+
+| 版本 | 日期 | 说明 |
+|------|------|------|
+| 1.0.0 | 2025-02-09 | 初版,基于 PRD 和 Flutter 代码分析 |
+
+---
+
+*本文档由 Claude Code 自动生成,如有问题请联系开发团队。*
From b54fbc1ccb8d03d0e6c6fcc0b7e23ac4313d109f Mon Sep 17 00:00:00 2001
From: zyc <1439655764@qq.com>
Date: Mon, 9 Feb 2026 18:03:55 +0800
Subject: [PATCH 2/5] add some api
---
airhub_app/.metadata | 18 +-
airhub_app/build.yaml | 3 +
airhub_app/lib/core/errors/exceptions.dart | 20 +
airhub_app/lib/core/network/api_client.dart | 178 +++
airhub_app/lib/core/network/api_client.g.dart | 51 +
airhub_app/lib/core/network/api_config.dart | 16 +
.../lib/core/network/token_manager.dart | 51 +
.../lib/core/network/token_manager.g.dart | 51 +
airhub_app/lib/core/router/app_router.dart | 18 +-
airhub_app/lib/core/router/app_router.g.dart | 2 +-
.../lib/core/services/phone_auth_service.dart | 85 ++
.../core/services/phone_auth_service.g.dart | 56 +
.../services/phone_auth_service_stub.dart | 14 +
.../datasources/auth_remote_data_source.dart | 71 +-
.../auth_remote_data_source.g.dart | 2 +-
.../repositories/auth_repository_impl.dart | 106 +-
.../repositories/auth_repository_impl.g.dart | 4 +-
.../auth/domain/entities/auth_tokens.dart | 15 +
.../domain/entities/auth_tokens.freezed.dart | 280 +++++
.../auth/domain/entities/auth_tokens.g.dart | 15 +
.../features/auth/domain/entities/user.dart | 8 +-
.../auth/domain/entities/user.freezed.dart | 56 +-
.../features/auth/domain/entities/user.g.dart | 14 +-
.../domain/repositories/auth_repository.dart | 7 +-
.../controllers/auth_controller.dart | 60 +-
.../controllers/auth_controller.g.dart | 2 +-
.../auth/presentation/pages/login_page.dart | 63 +-
.../device_remote_data_source.dart | 115 ++
.../device_remote_data_source.g.dart | 58 +
.../repositories/device_repository_impl.dart | 147 +++
.../device_repository_impl.g.dart | 56 +
.../device/domain/entities/device.dart | 55 +
.../domain/entities/device.freezed.dart | 932 ++++++++++++++
.../device/domain/entities/device.g.dart | 80 ++
.../device/domain/entities/device_detail.dart | 51 +
.../entities/device_detail.freezed.dart | 892 +++++++++++++
.../domain/entities/device_detail.g.dart | 76 ++
.../repositories/device_repository.dart | 16 +
.../controllers/device_controller.dart | 106 ++
.../controllers/device_controller.g.dart | 163 +++
.../notification_remote_data_source.dart | 74 ++
.../notification_remote_data_source.g.dart | 59 +
.../notification_repository_impl.dart | 78 ++
.../notification_repository_impl.g.dart | 58 +
.../domain/entities/app_notification.dart | 21 +
.../entities/app_notification.freezed.dart | 300 +++++
.../domain/entities/app_notification.g.dart | 31 +
.../repositories/notification_repository.dart | 11 +
.../controllers/notification_controller.dart | 75 ++
.../notification_controller.g.dart | 63 +
.../spirit_remote_data_source.dart | 71 ++
.../spirit_remote_data_source.g.dart | 58 +
.../repositories/spirit_repository_impl.dart | 116 ++
.../spirit_repository_impl.g.dart | 56 +
.../spirit/domain/entities/spirit.dart | 21 +
.../domain/entities/spirit.freezed.dart | 301 +++++
.../spirit/domain/entities/spirit.g.dart | 31 +
.../repositories/spirit_repository.dart | 19 +
.../controllers/spirit_controller.dart | 85 ++
.../controllers/spirit_controller.g.dart | 55 +
.../system_remote_data_source.dart | 50 +
.../system_remote_data_source.g.dart | 58 +
.../datasources/user_remote_data_source.dart | 45 +
.../user_remote_data_source.g.dart | 58 +
.../repositories/user_repository_impl.dart | 65 +
.../repositories/user_repository_impl.g.dart | 51 +
.../domain/repositories/user_repository.dart | 13 +
.../controllers/user_controller.dart | 64 +
.../controllers/user_controller.g.dart | 55 +
airhub_app/lib/main.dart | 1 +
airhub_app/lib/pages/device_control_page.dart | 170 +--
.../lib/pages/profile/agent_manage_page.dart | 182 ++-
.../lib/pages/profile/notification_page.dart | 321 +++--
.../lib/pages/profile/profile_info_page.dart | 145 ++-
.../lib/pages/profile/profile_page.dart | 42 +-
.../lib/pages/profile/settings_page.dart | 53 +-
airhub_app/lib/pages/settings_page.dart | 123 +-
airhub_app/lib/pages/wifi_config_page.dart | 20 +-
airhub_app/lib/widgets/feedback_dialog.dart | 100 +-
airhub_app/macos/.gitignore | 7 +
.../macos/Flutter/Flutter-Debug.xcconfig | 2 +
.../macos/Flutter/Flutter-Release.xcconfig | 2 +
.../Flutter/GeneratedPluginRegistrant.swift | 22 +
airhub_app/macos/Podfile | 42 +
airhub_app/macos/Podfile.lock | 56 +
.../macos/Runner.xcodeproj/project.pbxproj | 801 ++++++++++++
.../xcshareddata/IDEWorkspaceChecks.plist | 8 +
.../xcshareddata/xcschemes/Runner.xcscheme | 99 ++
.../contents.xcworkspacedata | 10 +
.../xcshareddata/IDEWorkspaceChecks.plist | 8 +
airhub_app/macos/Runner/AppDelegate.swift | 13 +
.../AppIcon.appiconset/Contents.json | 68 +
.../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 102994 bytes
.../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 5680 bytes
.../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 520 bytes
.../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 14142 bytes
.../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1066 bytes
.../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 36406 bytes
.../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 2218 bytes
.../macos/Runner/Base.lproj/MainMenu.xib | 343 +++++
.../macos/Runner/Configs/AppInfo.xcconfig | 14 +
.../macos/Runner/Configs/Debug.xcconfig | 2 +
.../macos/Runner/Configs/Release.xcconfig | 2 +
.../macos/Runner/Configs/Warnings.xcconfig | 13 +
.../macos/Runner/DebugProfile.entitlements | 14 +
airhub_app/macos/Runner/Info.plist | 32 +
.../macos/Runner/MainFlutterWindow.swift | 15 +
airhub_app/macos/Runner/Release.entitlements | 10 +
.../macos/RunnerTests/RunnerTests.swift | 12 +
.../packages/ali_auth/lib/ali_auth.dart | 107 ++
.../packages/ali_auth/lib/ali_auth_enum.dart | 242 ++++
.../ali_auth/lib/ali_auth_method_channel.dart | 161 +++
.../packages/ali_auth/lib/ali_auth_model.dart | 1114 +++++++++++++++++
.../lib/ali_auth_platform_interface.dart | 118 ++
.../packages/ali_auth/lib/ali_auth_web.dart | 48 +
.../ali_auth/lib/ali_auth_web_api.dart | 16 +
.../ali_auth/lib/ali_auth_web_utils.dart | 31 +
airhub_app/packages/ali_auth/pubspec.lock | 218 ++++
airhub_app/packages/ali_auth/pubspec.yaml | 78 ++
airhub_app/pubspec.lock | 397 +++---
airhub_app/pubspec.yaml | 11 +
121 files changed, 10661 insertions(+), 687 deletions(-)
create mode 100644 airhub_app/lib/core/errors/exceptions.dart
create mode 100644 airhub_app/lib/core/network/api_client.dart
create mode 100644 airhub_app/lib/core/network/api_client.g.dart
create mode 100644 airhub_app/lib/core/network/api_config.dart
create mode 100644 airhub_app/lib/core/network/token_manager.dart
create mode 100644 airhub_app/lib/core/network/token_manager.g.dart
create mode 100644 airhub_app/lib/core/services/phone_auth_service.dart
create mode 100644 airhub_app/lib/core/services/phone_auth_service.g.dart
create mode 100644 airhub_app/lib/core/services/phone_auth_service_stub.dart
create mode 100644 airhub_app/lib/features/auth/domain/entities/auth_tokens.dart
create mode 100644 airhub_app/lib/features/auth/domain/entities/auth_tokens.freezed.dart
create mode 100644 airhub_app/lib/features/auth/domain/entities/auth_tokens.g.dart
create mode 100644 airhub_app/lib/features/device/data/datasources/device_remote_data_source.dart
create mode 100644 airhub_app/lib/features/device/data/datasources/device_remote_data_source.g.dart
create mode 100644 airhub_app/lib/features/device/data/repositories/device_repository_impl.dart
create mode 100644 airhub_app/lib/features/device/data/repositories/device_repository_impl.g.dart
create mode 100644 airhub_app/lib/features/device/domain/entities/device.dart
create mode 100644 airhub_app/lib/features/device/domain/entities/device.freezed.dart
create mode 100644 airhub_app/lib/features/device/domain/entities/device.g.dart
create mode 100644 airhub_app/lib/features/device/domain/entities/device_detail.dart
create mode 100644 airhub_app/lib/features/device/domain/entities/device_detail.freezed.dart
create mode 100644 airhub_app/lib/features/device/domain/entities/device_detail.g.dart
create mode 100644 airhub_app/lib/features/device/domain/repositories/device_repository.dart
create mode 100644 airhub_app/lib/features/device/presentation/controllers/device_controller.dart
create mode 100644 airhub_app/lib/features/device/presentation/controllers/device_controller.g.dart
create mode 100644 airhub_app/lib/features/notification/data/datasources/notification_remote_data_source.dart
create mode 100644 airhub_app/lib/features/notification/data/datasources/notification_remote_data_source.g.dart
create mode 100644 airhub_app/lib/features/notification/data/repositories/notification_repository_impl.dart
create mode 100644 airhub_app/lib/features/notification/data/repositories/notification_repository_impl.g.dart
create mode 100644 airhub_app/lib/features/notification/domain/entities/app_notification.dart
create mode 100644 airhub_app/lib/features/notification/domain/entities/app_notification.freezed.dart
create mode 100644 airhub_app/lib/features/notification/domain/entities/app_notification.g.dart
create mode 100644 airhub_app/lib/features/notification/domain/repositories/notification_repository.dart
create mode 100644 airhub_app/lib/features/notification/presentation/controllers/notification_controller.dart
create mode 100644 airhub_app/lib/features/notification/presentation/controllers/notification_controller.g.dart
create mode 100644 airhub_app/lib/features/spirit/data/datasources/spirit_remote_data_source.dart
create mode 100644 airhub_app/lib/features/spirit/data/datasources/spirit_remote_data_source.g.dart
create mode 100644 airhub_app/lib/features/spirit/data/repositories/spirit_repository_impl.dart
create mode 100644 airhub_app/lib/features/spirit/data/repositories/spirit_repository_impl.g.dart
create mode 100644 airhub_app/lib/features/spirit/domain/entities/spirit.dart
create mode 100644 airhub_app/lib/features/spirit/domain/entities/spirit.freezed.dart
create mode 100644 airhub_app/lib/features/spirit/domain/entities/spirit.g.dart
create mode 100644 airhub_app/lib/features/spirit/domain/repositories/spirit_repository.dart
create mode 100644 airhub_app/lib/features/spirit/presentation/controllers/spirit_controller.dart
create mode 100644 airhub_app/lib/features/spirit/presentation/controllers/spirit_controller.g.dart
create mode 100644 airhub_app/lib/features/system/data/datasources/system_remote_data_source.dart
create mode 100644 airhub_app/lib/features/system/data/datasources/system_remote_data_source.g.dart
create mode 100644 airhub_app/lib/features/user/data/datasources/user_remote_data_source.dart
create mode 100644 airhub_app/lib/features/user/data/datasources/user_remote_data_source.g.dart
create mode 100644 airhub_app/lib/features/user/data/repositories/user_repository_impl.dart
create mode 100644 airhub_app/lib/features/user/data/repositories/user_repository_impl.g.dart
create mode 100644 airhub_app/lib/features/user/domain/repositories/user_repository.dart
create mode 100644 airhub_app/lib/features/user/presentation/controllers/user_controller.dart
create mode 100644 airhub_app/lib/features/user/presentation/controllers/user_controller.g.dart
create mode 100644 airhub_app/macos/.gitignore
create mode 100644 airhub_app/macos/Flutter/Flutter-Debug.xcconfig
create mode 100644 airhub_app/macos/Flutter/Flutter-Release.xcconfig
create mode 100644 airhub_app/macos/Flutter/GeneratedPluginRegistrant.swift
create mode 100644 airhub_app/macos/Podfile
create mode 100644 airhub_app/macos/Podfile.lock
create mode 100644 airhub_app/macos/Runner.xcodeproj/project.pbxproj
create mode 100644 airhub_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
create mode 100644 airhub_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
create mode 100644 airhub_app/macos/Runner.xcworkspace/contents.xcworkspacedata
create mode 100644 airhub_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
create mode 100644 airhub_app/macos/Runner/AppDelegate.swift
create mode 100644 airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
create mode 100644 airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
create mode 100644 airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
create mode 100644 airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
create mode 100644 airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
create mode 100644 airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
create mode 100644 airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
create mode 100644 airhub_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
create mode 100644 airhub_app/macos/Runner/Base.lproj/MainMenu.xib
create mode 100644 airhub_app/macos/Runner/Configs/AppInfo.xcconfig
create mode 100644 airhub_app/macos/Runner/Configs/Debug.xcconfig
create mode 100644 airhub_app/macos/Runner/Configs/Release.xcconfig
create mode 100644 airhub_app/macos/Runner/Configs/Warnings.xcconfig
create mode 100644 airhub_app/macos/Runner/DebugProfile.entitlements
create mode 100644 airhub_app/macos/Runner/Info.plist
create mode 100644 airhub_app/macos/Runner/MainFlutterWindow.swift
create mode 100644 airhub_app/macos/Runner/Release.entitlements
create mode 100644 airhub_app/macos/RunnerTests/RunnerTests.swift
create mode 100644 airhub_app/packages/ali_auth/lib/ali_auth.dart
create mode 100644 airhub_app/packages/ali_auth/lib/ali_auth_enum.dart
create mode 100644 airhub_app/packages/ali_auth/lib/ali_auth_method_channel.dart
create mode 100644 airhub_app/packages/ali_auth/lib/ali_auth_model.dart
create mode 100644 airhub_app/packages/ali_auth/lib/ali_auth_platform_interface.dart
create mode 100644 airhub_app/packages/ali_auth/lib/ali_auth_web.dart
create mode 100644 airhub_app/packages/ali_auth/lib/ali_auth_web_api.dart
create mode 100644 airhub_app/packages/ali_auth/lib/ali_auth_web_utils.dart
create mode 100644 airhub_app/packages/ali_auth/pubspec.lock
create mode 100644 airhub_app/packages/ali_auth/pubspec.yaml
diff --git a/airhub_app/.metadata b/airhub_app/.metadata
index 79e4428..d73f86e 100644
--- a/airhub_app/.metadata
+++ b/airhub_app/.metadata
@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
- revision: "67323de285b00232883f53b84095eb72be97d35c"
+ revision: "bd7a4a6b5576630823ca344e3e684c53aa1a0f46"
channel: "stable"
project_type: app
@@ -13,17 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
- create_revision: 67323de285b00232883f53b84095eb72be97d35c
- base_revision: 67323de285b00232883f53b84095eb72be97d35c
- - platform: android
- create_revision: 67323de285b00232883f53b84095eb72be97d35c
- base_revision: 67323de285b00232883f53b84095eb72be97d35c
- - platform: ios
- create_revision: 67323de285b00232883f53b84095eb72be97d35c
- base_revision: 67323de285b00232883f53b84095eb72be97d35c
- - platform: web
- create_revision: 67323de285b00232883f53b84095eb72be97d35c
- base_revision: 67323de285b00232883f53b84095eb72be97d35c
+ create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
+ base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
+ - platform: macos
+ create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
+ base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
# User provided section
diff --git a/airhub_app/build.yaml b/airhub_app/build.yaml
index bbac323..bb021b8 100644
--- a/airhub_app/build.yaml
+++ b/airhub_app/build.yaml
@@ -5,6 +5,9 @@ targets:
options:
build_extensions:
'^lib/{{}}.dart': 'lib/{{}}.g.dart'
+ json_serializable:
+ options:
+ field_rename: snake
freezed:
options:
build_extensions:
diff --git a/airhub_app/lib/core/errors/exceptions.dart b/airhub_app/lib/core/errors/exceptions.dart
new file mode 100644
index 0000000..9ebeabe
--- /dev/null
+++ b/airhub_app/lib/core/errors/exceptions.dart
@@ -0,0 +1,20 @@
+/// 服务端业务异常(后端返回 code != 0)
+class ServerException implements Exception {
+ final int code;
+ final String message;
+
+ const ServerException(this.code, this.message);
+
+ @override
+ String toString() => message;
+}
+
+/// 网络异常(无法连接服务器)
+class NetworkException implements Exception {
+ final String message;
+
+ const NetworkException([this.message = '网络连接失败,请检查网络']);
+
+ @override
+ String toString() => message;
+}
diff --git a/airhub_app/lib/core/network/api_client.dart b/airhub_app/lib/core/network/api_client.dart
new file mode 100644
index 0000000..8aebc65
--- /dev/null
+++ b/airhub_app/lib/core/network/api_client.dart
@@ -0,0 +1,178 @@
+import 'package:dio/dio.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+import '../errors/exceptions.dart';
+import 'api_config.dart';
+import 'token_manager.dart';
+
+part 'api_client.g.dart';
+
+@Riverpod(keepAlive: true)
+ApiClient apiClient(Ref ref) {
+ final tokenManager = ref.watch(tokenManagerProvider);
+ return ApiClient(tokenManager);
+}
+
+class ApiClient {
+ final TokenManager _tokenManager;
+ late final Dio _dio;
+
+ ApiClient(this._tokenManager) {
+ _dio = Dio(BaseOptions(
+ baseUrl: ApiConfig.fullBaseUrl,
+ connectTimeout: ApiConfig.connectTimeout,
+ receiveTimeout: ApiConfig.receiveTimeout,
+ headers: {'Content-Type': 'application/json'},
+ ));
+
+ _dio.interceptors.add(_AuthInterceptor(_tokenManager, _dio));
+ }
+
+ /// GET 请求,返回 data 字段
+ Future get(
+ String path, {
+ Map? queryParameters,
+ }) async {
+ final response = await _request(
+ () => _dio.get(path, queryParameters: queryParameters),
+ );
+ return response;
+ }
+
+ /// POST 请求,返回 data 字段
+ Future post(
+ String path, {
+ dynamic data,
+ }) async {
+ final response = await _request(
+ () => _dio.post(path, data: data),
+ );
+ return response;
+ }
+
+ /// PUT 请求,返回 data 字段
+ Future put(
+ String path, {
+ dynamic data,
+ }) async {
+ final response = await _request(
+ () => _dio.put(path, data: data),
+ );
+ return response;
+ }
+
+ /// DELETE 请求,返回 data 字段
+ Future delete(
+ String path, {
+ dynamic data,
+ }) async {
+ final response = await _request(
+ () => _dio.delete(path, data: data),
+ );
+ return response;
+ }
+
+ /// 统一请求处理:解析后端 {code, message, data} 格式
+ Future _request(Future Function() request) async {
+ try {
+ final response = await request();
+ final body = response.data;
+
+ if (body is Map) {
+ final code = body['code'] as int? ?? -1;
+ final message = body['message'] as String? ?? '未知错误';
+ final data = body['data'];
+
+ if (code == 0) {
+ return data;
+ } else {
+ throw ServerException(code, message);
+ }
+ }
+
+ return body;
+ } on DioException catch (e) {
+ if (e.response != null) {
+ final body = e.response?.data;
+ if (body is Map) {
+ final code = body['code'] as int? ?? -1;
+ final message = body['message'] as String? ?? '请求失败';
+ throw ServerException(code, message);
+ }
+ }
+ throw const NetworkException();
+ }
+ }
+}
+
+/// Auth 拦截器:自动附加 Bearer Token
+class _AuthInterceptor extends Interceptor {
+ final TokenManager _tokenManager;
+ final Dio _dio;
+
+ _AuthInterceptor(this._tokenManager, this._dio);
+
+ @override
+ void onRequest(
+ RequestOptions options,
+ RequestInterceptorHandler handler,
+ ) async {
+ // 不需要 token 的路径
+ const noAuthPaths = [
+ '/auth/send-code/',
+ '/auth/code-login/',
+ '/auth/phone-login/',
+ '/auth/refresh/',
+ '/version/check/',
+ ];
+
+ final needsAuth = !noAuthPaths.any((p) => options.path.contains(p));
+
+ if (needsAuth) {
+ final token = await _tokenManager.getAccessToken();
+ if (token != null && token.isNotEmpty) {
+ options.headers['Authorization'] = 'Bearer $token';
+ }
+ }
+
+ handler.next(options);
+ }
+
+ @override
+ void onError(DioException err, ErrorInterceptorHandler handler) async {
+ // 401: 尝试 refresh token
+ if (err.response?.statusCode == 401) {
+ try {
+ final refreshToken = await _tokenManager.getRefreshToken();
+ if (refreshToken != null) {
+ final response = await _dio.post(
+ '/auth/refresh/',
+ data: {'refresh': refreshToken},
+ options: Options(headers: {'Authorization': ''}),
+ );
+
+ final body = response.data;
+ if (body is Map && body['code'] == 0) {
+ final data = body['data'] as Map;
+ await _tokenManager.saveTokens(
+ access: data['access'] as String,
+ refresh: data['refresh'] as String,
+ );
+
+ // 用新 token 重试原请求
+ final opts = err.requestOptions;
+ opts.headers['Authorization'] =
+ 'Bearer ${data['access']}';
+ final retryResponse = await _dio.fetch(opts);
+ return handler.resolve(retryResponse);
+ }
+ }
+ } catch (_) {
+ // refresh 失败,清除 token(会触发路由守卫跳转登录)
+ await _tokenManager.clearTokens();
+ }
+ }
+
+ handler.next(err);
+ }
+}
diff --git a/airhub_app/lib/core/network/api_client.g.dart b/airhub_app/lib/core/network/api_client.g.dart
new file mode 100644
index 0000000..2d28050
--- /dev/null
+++ b/airhub_app/lib/core/network/api_client.g.dart
@@ -0,0 +1,51 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'api_client.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint, type=warning
+
+@ProviderFor(apiClient)
+const apiClientProvider = ApiClientProvider._();
+
+final class ApiClientProvider
+ extends $FunctionalProvider
+ with $Provider {
+ const ApiClientProvider._()
+ : super(
+ from: null,
+ argument: null,
+ retry: null,
+ name: r'apiClientProvider',
+ isAutoDispose: false,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$apiClientHash();
+
+ @$internal
+ @override
+ $ProviderElement $createElement($ProviderPointer pointer) =>
+ $ProviderElement(pointer);
+
+ @override
+ ApiClient create(Ref ref) {
+ return apiClient(ref);
+ }
+
+ /// {@macro riverpod.override_with_value}
+ Override overrideWithValue(ApiClient value) {
+ return $ProviderOverride(
+ origin: this,
+ providerOverride: $SyncValueProvider(value),
+ );
+ }
+}
+
+String _$apiClientHash() => r'9d0cce119ded498b0bdf8ec8bb1ed5fc9fcfb8aa';
diff --git a/airhub_app/lib/core/network/api_config.dart b/airhub_app/lib/core/network/api_config.dart
new file mode 100644
index 0000000..7abe3aa
--- /dev/null
+++ b/airhub_app/lib/core/network/api_config.dart
@@ -0,0 +1,16 @@
+class ApiConfig {
+ /// 后端服务器地址(开发环境请替换为实际 IP)
+ static const String baseUrl = 'http://127.0.0.1:8000';
+
+ /// App 端 API 前缀
+ static const String apiPrefix = '/api/v1';
+
+ /// 连接超时
+ static const Duration connectTimeout = Duration(seconds: 15);
+
+ /// 接收超时
+ static const Duration receiveTimeout = Duration(seconds: 15);
+
+ /// 拼接完整 URL
+ static String get fullBaseUrl => '$baseUrl$apiPrefix';
+}
diff --git a/airhub_app/lib/core/network/token_manager.dart b/airhub_app/lib/core/network/token_manager.dart
new file mode 100644
index 0000000..b3e654f
--- /dev/null
+++ b/airhub_app/lib/core/network/token_manager.dart
@@ -0,0 +1,51 @@
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+part 'token_manager.g.dart';
+
+const _keyAccessToken = 'access_token';
+const _keyRefreshToken = 'refresh_token';
+
+@Riverpod(keepAlive: true)
+TokenManager tokenManager(Ref ref) {
+ return TokenManager();
+}
+
+class TokenManager {
+ SharedPreferences? _prefs;
+
+ Future get _preferences async {
+ _prefs ??= await SharedPreferences.getInstance();
+ return _prefs!;
+ }
+
+ Future saveTokens({
+ required String access,
+ required String refresh,
+ }) async {
+ final prefs = await _preferences;
+ await prefs.setString(_keyAccessToken, access);
+ await prefs.setString(_keyRefreshToken, refresh);
+ }
+
+ Future getAccessToken() async {
+ final prefs = await _preferences;
+ return prefs.getString(_keyAccessToken);
+ }
+
+ Future getRefreshToken() async {
+ final prefs = await _preferences;
+ return prefs.getString(_keyRefreshToken);
+ }
+
+ Future hasToken() async {
+ final token = await getAccessToken();
+ return token != null && token.isNotEmpty;
+ }
+
+ Future clearTokens() async {
+ final prefs = await _preferences;
+ await prefs.remove(_keyAccessToken);
+ await prefs.remove(_keyRefreshToken);
+ }
+}
diff --git a/airhub_app/lib/core/network/token_manager.g.dart b/airhub_app/lib/core/network/token_manager.g.dart
new file mode 100644
index 0000000..dfc91b3
--- /dev/null
+++ b/airhub_app/lib/core/network/token_manager.g.dart
@@ -0,0 +1,51 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'token_manager.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint, type=warning
+
+@ProviderFor(tokenManager)
+const tokenManagerProvider = TokenManagerProvider._();
+
+final class TokenManagerProvider
+ extends $FunctionalProvider
+ with $Provider {
+ const TokenManagerProvider._()
+ : super(
+ from: null,
+ argument: null,
+ retry: null,
+ name: r'tokenManagerProvider',
+ isAutoDispose: false,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$tokenManagerHash();
+
+ @$internal
+ @override
+ $ProviderElement $createElement($ProviderPointer pointer) =>
+ $ProviderElement(pointer);
+
+ @override
+ TokenManager create(Ref ref) {
+ return tokenManager(ref);
+ }
+
+ /// {@macro riverpod.override_with_value}
+ Override overrideWithValue(TokenManager value) {
+ return $ProviderOverride(
+ origin: this,
+ providerOverride: $SyncValueProvider(value),
+ );
+ }
+}
+
+String _$tokenManagerHash() => r'94bb9e39530e1d18331ea750bd4b6c5d4f16f1e9';
diff --git a/airhub_app/lib/core/router/app_router.dart b/airhub_app/lib/core/router/app_router.dart
index 6b7c8d5..a2acb04 100644
--- a/airhub_app/lib/core/router/app_router.dart
+++ b/airhub_app/lib/core/router/app_router.dart
@@ -8,14 +8,28 @@ import '../../pages/home_page.dart';
import '../../pages/profile/profile_page.dart';
import '../../pages/webview_page.dart';
import '../../pages/wifi_config_page.dart';
+import '../network/token_manager.dart';
part 'app_router.g.dart';
@riverpod
GoRouter goRouter(Ref ref) {
+ final tokenManager = ref.watch(tokenManagerProvider);
+
return GoRouter(
- initialLocation:
- '/login', // Start at login for now, logic can be added to check auth state later
+ initialLocation: '/login',
+ redirect: (context, state) async {
+ final hasToken = await tokenManager.hasToken();
+ final isLoginRoute = state.matchedLocation == '/login';
+
+ if (!hasToken && !isLoginRoute) {
+ return '/login';
+ }
+ if (hasToken && isLoginRoute) {
+ return '/home';
+ }
+ return null;
+ },
routes: [
GoRoute(path: '/login', builder: (context, state) => const LoginPage()),
GoRoute(path: '/home', builder: (context, state) => const HomePage()),
diff --git a/airhub_app/lib/core/router/app_router.g.dart b/airhub_app/lib/core/router/app_router.g.dart
index 16ea3e2..631592a 100644
--- a/airhub_app/lib/core/router/app_router.g.dart
+++ b/airhub_app/lib/core/router/app_router.g.dart
@@ -48,4 +48,4 @@ final class GoRouterProvider
}
}
-String _$goRouterHash() => r'937320fb6893b1da17afec22844ae01cf2e22441';
+String _$goRouterHash() => r'b559a84bcf9ae1ffda1deba4cf213f31a4006782';
diff --git a/airhub_app/lib/core/services/phone_auth_service.dart b/airhub_app/lib/core/services/phone_auth_service.dart
new file mode 100644
index 0000000..cfd1177
--- /dev/null
+++ b/airhub_app/lib/core/services/phone_auth_service.dart
@@ -0,0 +1,85 @@
+import 'dart:async';
+import 'package:flutter/foundation.dart' show kIsWeb;
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+// 条件导入:Web 用 stub,原生用真实 ali_auth
+import 'phone_auth_service_stub.dart'
+ if (dart.library.io) 'package:ali_auth/ali_auth.dart';
+
+part 'phone_auth_service.g.dart';
+
+class PhoneAuthConfig {
+ static const String androidSk =
+ 'eLg3aWBZ2JOO6eX6bmrcSGqNSE3/jWafpIh1JYL2PIW4WxkQdrUUVCKFmeErwr+ZcqotmFc+zSDYZpyO6xVNfPB+C8KdaJwO19Sn7kbFX52Gv2T7neNTGXla+lwHj4lqpL5P2zmFNeZlTeYN9YuggsPc2IeX+6T3F26r1ih7HcCfMPECqUyTY9a0HT0CCsfwva1gfRAr2MN87I3yRGsr2IpPOBvGqUoa8cD9+8EBBQzouCZ5YbrE3MP2dISTHmx+8ORYEP6NT3BmPnPR6UVQEc6nTmbMMjjLMKFaMsi+M4gg5pgnEwYhd0GYB6oV+v15';
+ static const String iosSk =
+ 'kl3UL3GomT2sxglYyXY9LeuXAxxej24SotVP1UAZij4NI3T7E5W3NKFVv61E3bUwugtfRyucSDkws25tzZ9LoBGEg4H19MD31YmwxhYsKlS5fe/+jdigjDXsNWonTLEmFqxGJqtav2i0M9Q5L5YQcQpHYWc2IpL3WT2dTCU876ghQIm8UXF8TYhwHNGLvdjuUbp1naGjwbWxzov2Fy2b0DOkb5q1gc0DWsKQ4XAQhzwcivO88VbT/7tuFFdpGxmpwETBS0u7pkeGan2ZCKxAEA==';
+}
+
+@Riverpod(keepAlive: true)
+PhoneAuthService phoneAuthService(Ref ref) {
+ return PhoneAuthService();
+}
+
+class PhoneAuthService {
+ bool _initialized = false;
+
+ /// 初始化 SDK(只需调用一次)
+ Future init() async {
+ if (_initialized) return;
+ // 真机才初始化,Web 跳过
+ if (kIsWeb) return;
+
+ try {
+ await AliAuth.initSdk(
+ AliAuthModel(
+ PhoneAuthConfig.androidSk,
+ PhoneAuthConfig.iosSk,
+ isDebug: true,
+ autoQuitPage: true,
+ pageType: PageType.fullPort,
+ ),
+ );
+ _initialized = true;
+ } catch (e) {
+ // SDK 初始化失败不阻塞 App 启动
+ _initialized = false;
+ }
+ }
+
+ /// 一键登录,返回阿里云 token(用于发给后端换手机号)
+ /// 返回 null 表示用户取消或认证失败
+ Future getLoginToken() async {
+ if (!_initialized) {
+ await init();
+ }
+ if (!_initialized) return null;
+
+ final completer = Completer();
+
+ AliAuth.loginListen(onEvent: (event) {
+ final code = event['code'] as String?;
+
+ if (code == '600000' && event['data'] != null) {
+ // 成功获取 token
+ if (!completer.isCompleted) {
+ completer.complete(event['data'] as String);
+ }
+ } else if (code == '700000' || code == '700001') {
+ // 用户取消
+ if (!completer.isCompleted) {
+ completer.complete(null);
+ }
+ } else if (code != null && code.startsWith('6') && code != '600000') {
+ // 其他 6xxxxx 错误码
+ if (!completer.isCompleted) {
+ completer.complete(null);
+ }
+ }
+ });
+
+ return completer.future.timeout(
+ const Duration(seconds: 30),
+ onTimeout: () => null,
+ );
+ }
+}
diff --git a/airhub_app/lib/core/services/phone_auth_service.g.dart b/airhub_app/lib/core/services/phone_auth_service.g.dart
new file mode 100644
index 0000000..e9c6387
--- /dev/null
+++ b/airhub_app/lib/core/services/phone_auth_service.g.dart
@@ -0,0 +1,56 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'phone_auth_service.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint, type=warning
+
+@ProviderFor(phoneAuthService)
+const phoneAuthServiceProvider = PhoneAuthServiceProvider._();
+
+final class PhoneAuthServiceProvider
+ extends
+ $FunctionalProvider<
+ PhoneAuthService,
+ PhoneAuthService,
+ PhoneAuthService
+ >
+ with $Provider {
+ const PhoneAuthServiceProvider._()
+ : super(
+ from: null,
+ argument: null,
+ retry: null,
+ name: r'phoneAuthServiceProvider',
+ isAutoDispose: false,
+ dependencies: null,
+ $allTransitiveDependencies: null,
+ );
+
+ @override
+ String debugGetCreateSourceHash() => _$phoneAuthServiceHash();
+
+ @$internal
+ @override
+ $ProviderElement $createElement($ProviderPointer pointer) =>
+ $ProviderElement(pointer);
+
+ @override
+ PhoneAuthService create(Ref ref) {
+ return phoneAuthService(ref);
+ }
+
+ /// {@macro riverpod.override_with_value}
+ Override overrideWithValue(PhoneAuthService value) {
+ return $ProviderOverride(
+ origin: this,
+ providerOverride: $SyncValueProvider(value),
+ );
+ }
+}
+
+String _$phoneAuthServiceHash() => r'b7a4a2481eef4a3ddd0ce529c879dc5319a3106b';
diff --git a/airhub_app/lib/core/services/phone_auth_service_stub.dart b/airhub_app/lib/core/services/phone_auth_service_stub.dart
new file mode 100644
index 0000000..802866f
--- /dev/null
+++ b/airhub_app/lib/core/services/phone_auth_service_stub.dart
@@ -0,0 +1,14 @@
+/// Web 平台 stub — ali_auth 不支持 Web,提供空实现
+class AliAuth {
+ static Future initSdk(dynamic model) async {}
+ static void loginListen({required Function(Map) onEvent}) {}
+}
+
+class AliAuthModel {
+ AliAuthModel(String androidSk, String iosSk,
+ {bool isDebug = false, bool autoQuitPage = false, dynamic pageType});
+}
+
+class PageType {
+ static const fullPort = 0;
+}
diff --git a/airhub_app/lib/features/auth/data/datasources/auth_remote_data_source.dart b/airhub_app/lib/features/auth/data/datasources/auth_remote_data_source.dart
index 5bb74a9..83c714c 100644
--- a/airhub_app/lib/features/auth/data/datasources/auth_remote_data_source.dart
+++ b/airhub_app/lib/features/auth/data/datasources/auth_remote_data_source.dart
@@ -1,39 +1,72 @@
-import 'dart:async';
import 'package:riverpod_annotation/riverpod_annotation.dart';
+import '../../../../core/network/api_client.dart';
+import '../../domain/entities/auth_tokens.dart';
import '../../domain/entities/user.dart';
part 'auth_remote_data_source.g.dart';
abstract class AuthRemoteDataSource {
- Future loginWithPhone(String phoneNumber, String code);
- Future oneClickLogin();
+ Future sendCode(String phone);
+ Future<({User user, AuthTokens tokens, bool isNewUser})> codeLogin(
+ String phone, String code);
+ Future<({User user, AuthTokens tokens, bool isNewUser})> tokenLogin(
+ String token);
+ Future logout(String refreshToken);
+ Future deleteAccount();
}
@riverpod
AuthRemoteDataSource authRemoteDataSource(Ref ref) {
- return AuthRemoteDataSourceImpl();
+ final apiClient = ref.watch(apiClientProvider);
+ return AuthRemoteDataSourceImpl(apiClient);
}
class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
+ final ApiClient _apiClient;
+
+ AuthRemoteDataSourceImpl(this._apiClient);
+
@override
- Future loginWithPhone(String phoneNumber, String code) async {
- // Mock network delay and logic copied from original login_page.dart
- await Future.delayed(const Duration(milliseconds: 1500));
- // Simulate successful login
- return User(
- id: '1',
- phoneNumber: phoneNumber,
- nickname: 'User ${phoneNumber.substring(7)}',
- );
+ Future sendCode(String phone) async {
+ await _apiClient.post('/auth/send-code/', data: {'phone': phone});
}
@override
- Future oneClickLogin() async {
- await Future.delayed(const Duration(milliseconds: 1500));
- return const User(
- id: '2',
- phoneNumber: '13800138000',
- nickname: 'OneClick User',
+ Future<({User user, AuthTokens tokens, bool isNewUser})> codeLogin(
+ String phone, String code) async {
+ final data = await _apiClient.post(
+ '/auth/code-login/',
+ data: {'phone': phone, 'code': code},
);
+ return _parseLoginResponse(data as Map);
+ }
+
+ @override
+ Future<({User user, AuthTokens tokens, bool isNewUser})> tokenLogin(
+ String token) async {
+ final data = await _apiClient.post(
+ '/auth/phone-login/',
+ data: {'token': token},
+ );
+ return _parseLoginResponse(data as Map);
+ }
+
+ @override
+ Future logout(String refreshToken) async {
+ await _apiClient.post('/auth/logout/', data: {'refresh': refreshToken});
+ }
+
+ @override
+ Future deleteAccount() async {
+ await _apiClient.delete('/auth/account/');
+ }
+
+ ({User user, AuthTokens tokens, bool isNewUser}) _parseLoginResponse(
+ Map data) {
+ final user = User.fromJson(data['user'] as Map);
+ // 后端返回字段名为 'token'(非 'tokens')
+ final tokens = AuthTokens.fromJson(data['token'] as Map);
+ final isNewUser = data['is_new_user'] as bool? ?? false;
+ return (user: user, tokens: tokens, isNewUser: isNewUser);
}
}
diff --git a/airhub_app/lib/features/auth/data/datasources/auth_remote_data_source.g.dart b/airhub_app/lib/features/auth/data/datasources/auth_remote_data_source.g.dart
index d0872c1..41e3fc8 100644
--- a/airhub_app/lib/features/auth/data/datasources/auth_remote_data_source.g.dart
+++ b/airhub_app/lib/features/auth/data/datasources/auth_remote_data_source.g.dart
@@ -55,4 +55,4 @@ final class AuthRemoteDataSourceProvider
}
String _$authRemoteDataSourceHash() =>
- r'b6a9edd1b6c48be8564688bac362316f598b4432';
+ r'9f874814620b5a8bcdf56417a68ed7cba404a9e9';
diff --git a/airhub_app/lib/features/auth/data/repositories/auth_repository_impl.dart b/airhub_app/lib/features/auth/data/repositories/auth_repository_impl.dart
index ce2fc96..c2d0eaf 100644
--- a/airhub_app/lib/features/auth/data/repositories/auth_repository_impl.dart
+++ b/airhub_app/lib/features/auth/data/repositories/auth_repository_impl.dart
@@ -1,51 +1,121 @@
+import 'dart:async';
import 'package:fpdart/fpdart.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
+import '../../../../core/errors/exceptions.dart';
import '../../../../core/errors/failures.dart';
+import '../../../../core/network/token_manager.dart';
import '../../domain/entities/user.dart';
import '../../domain/repositories/auth_repository.dart';
import '../datasources/auth_remote_data_source.dart';
part 'auth_repository_impl.g.dart';
-@riverpod
+@Riverpod(keepAlive: true)
AuthRepository authRepository(Ref ref) {
final remoteDataSource = ref.watch(authRemoteDataSourceProvider);
- return AuthRepositoryImpl(remoteDataSource);
+ final tokenManager = ref.watch(tokenManagerProvider);
+ return AuthRepositoryImpl(remoteDataSource, tokenManager);
}
class AuthRepositoryImpl implements AuthRepository {
final AuthRemoteDataSource _remoteDataSource;
+ final TokenManager _tokenManager;
+ final _authStateController = StreamController.broadcast();
+ User? _currentUser;
- AuthRepositoryImpl(this._remoteDataSource);
+ AuthRepositoryImpl(this._remoteDataSource, this._tokenManager);
@override
- Stream get authStateChanges => Stream.value(null); // Mock stream
+ User? get currentUser => _currentUser;
@override
- Future> loginWithPhone(
- String phoneNumber,
- String code,
- ) async {
+ Stream get authStateChanges => _authStateController.stream;
+
+ @override
+ Future> sendCode(String phone) async {
try {
- final user = await _remoteDataSource.loginWithPhone(phoneNumber, code);
- return right(user);
- } catch (e) {
- return left(const ServerFailure('Login failed'));
+ await _remoteDataSource.sendCode(phone);
+ return right(null);
+ } on ServerException catch (e) {
+ return left(ServerFailure(e.message));
+ } on NetworkException catch (e) {
+ return left(NetworkFailure(e.message));
}
}
@override
- Future> oneClickLogin() async {
+ Future> codeLogin(String phone, String code) async {
try {
- final user = await _remoteDataSource.oneClickLogin();
- return right(user);
- } catch (e) {
- return left(const ServerFailure('One-click login failed'));
+ final result = await _remoteDataSource.codeLogin(phone, code);
+ await _tokenManager.saveTokens(
+ access: result.tokens.access,
+ refresh: result.tokens.refresh,
+ );
+ _currentUser = result.user;
+ _authStateController.add(result.user);
+ return right(result.user);
+ } on ServerException catch (e) {
+ return left(ServerFailure(e.message));
+ } on NetworkException catch (e) {
+ return left(NetworkFailure(e.message));
+ }
+ }
+
+ @override
+ Future> tokenLogin(String token) async {
+ try {
+ final result = await _remoteDataSource.tokenLogin(token);
+ await _tokenManager.saveTokens(
+ access: result.tokens.access,
+ refresh: result.tokens.refresh,
+ );
+ _currentUser = result.user;
+ _authStateController.add(result.user);
+ return right(result.user);
+ } on ServerException catch (e) {
+ return left(ServerFailure(e.message));
+ } on NetworkException catch (e) {
+ return left(NetworkFailure(e.message));
}
}
@override
Future> logout() async {
- return right(null);
+ try {
+ final refreshToken = await _tokenManager.getRefreshToken();
+ if (refreshToken != null) {
+ await _remoteDataSource.logout(refreshToken);
+ }
+ await _tokenManager.clearTokens();
+ _currentUser = null;
+ _authStateController.add(null);
+ return right(null);
+ } on ServerException catch (_) {
+ // 即使 API 失败也清除本地 token
+ await _tokenManager.clearTokens();
+ _currentUser = null;
+ _authStateController.add(null);
+ return right(null);
+ } on NetworkException catch (_) {
+ await _tokenManager.clearTokens();
+ _currentUser = null;
+ _authStateController.add(null);
+ return right(null);
+ }
+ }
+
+ @override
+ Future> deleteAccount() async {
+ try {
+ await _remoteDataSource.deleteAccount();
+ await _tokenManager.clearTokens();
+ _currentUser = null;
+ _authStateController.add(null);
+ return right(null);
+ } on ServerException catch (e) {
+ return left(ServerFailure(e.message));
+ } on NetworkException catch (e) {
+ return left(NetworkFailure(e.message));
+ }
}
}
diff --git a/airhub_app/lib/features/auth/data/repositories/auth_repository_impl.g.dart b/airhub_app/lib/features/auth/data/repositories/auth_repository_impl.g.dart
index dba5767..9cebe11 100644
--- a/airhub_app/lib/features/auth/data/repositories/auth_repository_impl.g.dart
+++ b/airhub_app/lib/features/auth/data/repositories/auth_repository_impl.g.dart
@@ -21,7 +21,7 @@ final class AuthRepositoryProvider
argument: null,
retry: null,
name: r'authRepositoryProvider',
- isAutoDispose: true,
+ isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@@ -48,4 +48,4 @@ final class AuthRepositoryProvider
}
}
-String _$authRepositoryHash() => r'43e05b07a705006cf920b080f78421ecc8bab1d9';
+String _$authRepositoryHash() => r'4f799a30b753954fa92b1ed1b21277bd6020a4a0';
diff --git a/airhub_app/lib/features/auth/domain/entities/auth_tokens.dart b/airhub_app/lib/features/auth/domain/entities/auth_tokens.dart
new file mode 100644
index 0000000..cd43231
--- /dev/null
+++ b/airhub_app/lib/features/auth/domain/entities/auth_tokens.dart
@@ -0,0 +1,15 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'auth_tokens.freezed.dart';
+part 'auth_tokens.g.dart';
+
+@freezed
+abstract class AuthTokens with _$AuthTokens {
+ const factory AuthTokens({
+ required String access,
+ required String refresh,
+ }) = _AuthTokens;
+
+ factory AuthTokens.fromJson(Map json) =>
+ _$AuthTokensFromJson(json);
+}
diff --git a/airhub_app/lib/features/auth/domain/entities/auth_tokens.freezed.dart b/airhub_app/lib/features/auth/domain/entities/auth_tokens.freezed.dart
new file mode 100644
index 0000000..9dbd7b7
--- /dev/null
+++ b/airhub_app/lib/features/auth/domain/entities/auth_tokens.freezed.dart
@@ -0,0 +1,280 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// coverage:ignore-file
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'auth_tokens.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+// dart format off
+T _$identity(T value) => value;
+
+/// @nodoc
+mixin _$AuthTokens {
+
+ String get access; String get refresh;
+/// Create a copy of AuthTokens
+/// with the given fields replaced by the non-null parameter values.
+@JsonKey(includeFromJson: false, includeToJson: false)
+@pragma('vm:prefer-inline')
+$AuthTokensCopyWith get copyWith => _$AuthTokensCopyWithImpl(this as AuthTokens, _$identity);
+
+ /// Serializes this AuthTokens to a JSON map.
+ Map toJson();
+
+
+@override
+bool operator ==(Object other) {
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is AuthTokens&&(identical(other.access, access) || other.access == access)&&(identical(other.refresh, refresh) || other.refresh == refresh));
+}
+
+@JsonKey(includeFromJson: false, includeToJson: false)
+@override
+int get hashCode => Object.hash(runtimeType,access,refresh);
+
+@override
+String toString() {
+ return 'AuthTokens(access: $access, refresh: $refresh)';
+}
+
+
+}
+
+/// @nodoc
+abstract mixin class $AuthTokensCopyWith<$Res> {
+ factory $AuthTokensCopyWith(AuthTokens value, $Res Function(AuthTokens) _then) = _$AuthTokensCopyWithImpl;
+@useResult
+$Res call({
+ String access, String refresh
+});
+
+
+
+
+}
+/// @nodoc
+class _$AuthTokensCopyWithImpl<$Res>
+ implements $AuthTokensCopyWith<$Res> {
+ _$AuthTokensCopyWithImpl(this._self, this._then);
+
+ final AuthTokens _self;
+ final $Res Function(AuthTokens) _then;
+
+/// Create a copy of AuthTokens
+/// with the given fields replaced by the non-null parameter values.
+@pragma('vm:prefer-inline') @override $Res call({Object? access = null,Object? refresh = null,}) {
+ return _then(_self.copyWith(
+access: null == access ? _self.access : access // ignore: cast_nullable_to_non_nullable
+as String,refresh: null == refresh ? _self.refresh : refresh // ignore: cast_nullable_to_non_nullable
+as String,
+ ));
+}
+
+}
+
+
+/// Adds pattern-matching-related methods to [AuthTokens].
+extension AuthTokensPatterns on AuthTokens {
+/// A variant of `map` that fallback to returning `orElse`.
+///
+/// It is equivalent to doing:
+/// ```dart
+/// switch (sealedClass) {
+/// case final Subclass value:
+/// return ...;
+/// case _:
+/// return orElse();
+/// }
+/// ```
+
+@optionalTypeArgs TResult maybeMap(TResult Function( _AuthTokens value)? $default,{required TResult orElse(),}){
+final _that = this;
+switch (_that) {
+case _AuthTokens() when $default != null:
+return $default(_that);case _:
+ return orElse();
+
+}
+}
+/// A `switch`-like method, using callbacks.
+///
+/// Callbacks receives the raw object, upcasted.
+/// It is equivalent to doing:
+/// ```dart
+/// switch (sealedClass) {
+/// case final Subclass value:
+/// return ...;
+/// case final Subclass2 value:
+/// return ...;
+/// }
+/// ```
+
+@optionalTypeArgs TResult map(TResult Function( _AuthTokens value) $default,){
+final _that = this;
+switch (_that) {
+case _AuthTokens():
+return $default(_that);case _:
+ throw StateError('Unexpected subclass');
+
+}
+}
+/// A variant of `map` that fallback to returning `null`.
+///
+/// It is equivalent to doing:
+/// ```dart
+/// switch (sealedClass) {
+/// case final Subclass value:
+/// return ...;
+/// case _:
+/// return null;
+/// }
+/// ```
+
+@optionalTypeArgs TResult? mapOrNull(TResult? Function( _AuthTokens value)? $default,){
+final _that = this;
+switch (_that) {
+case _AuthTokens() when $default != null:
+return $default(_that);case _:
+ return null;
+
+}
+}
+/// A variant of `when` that fallback to an `orElse` callback.
+///
+/// It is equivalent to doing:
+/// ```dart
+/// switch (sealedClass) {
+/// case Subclass(:final field):
+/// return ...;
+/// case _:
+/// return orElse();
+/// }
+/// ```
+
+@optionalTypeArgs TResult maybeWhen(TResult Function( String access, String refresh)? $default,{required TResult orElse(),}) {final _that = this;
+switch (_that) {
+case _AuthTokens() when $default != null:
+return $default(_that.access,_that.refresh);case _:
+ return orElse();
+
+}
+}
+/// A `switch`-like method, using callbacks.
+///
+/// As opposed to `map`, this offers destructuring.
+/// It is equivalent to doing:
+/// ```dart
+/// switch (sealedClass) {
+/// case Subclass(:final field):
+/// return ...;
+/// case Subclass2(:final field2):
+/// return ...;
+/// }
+/// ```
+
+@optionalTypeArgs TResult when(TResult Function( String access, String refresh) $default,) {final _that = this;
+switch (_that) {
+case _AuthTokens():
+return $default(_that.access,_that.refresh);case _:
+ throw StateError('Unexpected subclass');
+
+}
+}
+/// A variant of `when` that fallback to returning `null`
+///
+/// It is equivalent to doing:
+/// ```dart
+/// switch (sealedClass) {
+/// case Subclass(:final field):
+/// return ...;
+/// case _:
+/// return null;
+/// }
+/// ```
+
+@optionalTypeArgs TResult? whenOrNull(TResult? Function( String access, String refresh)? $default,) {final _that = this;
+switch (_that) {
+case _AuthTokens() when $default != null:
+return $default(_that.access,_that.refresh);case _:
+ return null;
+
+}
+}
+
+}
+
+/// @nodoc
+@JsonSerializable()
+
+class _AuthTokens implements AuthTokens {
+ const _AuthTokens({required this.access, required this.refresh});
+ factory _AuthTokens.fromJson(Map json) => _$AuthTokensFromJson(json);
+
+@override final String access;
+@override final String refresh;
+
+/// Create a copy of AuthTokens
+/// with the given fields replaced by the non-null parameter values.
+@override @JsonKey(includeFromJson: false, includeToJson: false)
+@pragma('vm:prefer-inline')
+_$AuthTokensCopyWith<_AuthTokens> get copyWith => __$AuthTokensCopyWithImpl<_AuthTokens>(this, _$identity);
+
+@override
+Map toJson() {
+ return _$AuthTokensToJson(this, );
+}
+
+@override
+bool operator ==(Object other) {
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is _AuthTokens&&(identical(other.access, access) || other.access == access)&&(identical(other.refresh, refresh) || other.refresh == refresh));
+}
+
+@JsonKey(includeFromJson: false, includeToJson: false)
+@override
+int get hashCode => Object.hash(runtimeType,access,refresh);
+
+@override
+String toString() {
+ return 'AuthTokens(access: $access, refresh: $refresh)';
+}
+
+
+}
+
+/// @nodoc
+abstract mixin class _$AuthTokensCopyWith<$Res> implements $AuthTokensCopyWith<$Res> {
+ factory _$AuthTokensCopyWith(_AuthTokens value, $Res Function(_AuthTokens) _then) = __$AuthTokensCopyWithImpl;
+@override @useResult
+$Res call({
+ String access, String refresh
+});
+
+
+
+
+}
+/// @nodoc
+class __$AuthTokensCopyWithImpl<$Res>
+ implements _$AuthTokensCopyWith<$Res> {
+ __$AuthTokensCopyWithImpl(this._self, this._then);
+
+ final _AuthTokens _self;
+ final $Res Function(_AuthTokens) _then;
+
+/// Create a copy of AuthTokens
+/// with the given fields replaced by the non-null parameter values.
+@override @pragma('vm:prefer-inline') $Res call({Object? access = null,Object? refresh = null,}) {
+ return _then(_AuthTokens(
+access: null == access ? _self.access : access // ignore: cast_nullable_to_non_nullable
+as String,refresh: null == refresh ? _self.refresh : refresh // ignore: cast_nullable_to_non_nullable
+as String,
+ ));
+}
+
+
+}
+
+// dart format on
diff --git a/airhub_app/lib/features/auth/domain/entities/auth_tokens.g.dart b/airhub_app/lib/features/auth/domain/entities/auth_tokens.g.dart
new file mode 100644
index 0000000..e051bc7
--- /dev/null
+++ b/airhub_app/lib/features/auth/domain/entities/auth_tokens.g.dart
@@ -0,0 +1,15 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'auth_tokens.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+_AuthTokens _$AuthTokensFromJson(Map json) => _AuthTokens(
+ access: json['access'] as String,
+ refresh: json['refresh'] as String,
+);
+
+Map _$AuthTokensToJson(_AuthTokens instance) =>
+ {'access': instance.access, 'refresh': instance.refresh};
diff --git a/airhub_app/lib/features/auth/domain/entities/user.dart b/airhub_app/lib/features/auth/domain/entities/user.dart
index 663a2e5..5e069e7 100644
--- a/airhub_app/lib/features/auth/domain/entities/user.dart
+++ b/airhub_app/lib/features/auth/domain/entities/user.dart
@@ -6,10 +6,12 @@ part 'user.g.dart';
@freezed
abstract class User with _$User {
const factory User({
- required String id,
- required String phoneNumber,
+ required int id,
+ required String phone,
String? nickname,
- String? avatarUrl,
+ String? avatar,
+ String? gender,
+ String? birthday,
}) = _User;
factory User.fromJson(Map json) => _$UserFromJson(json);
diff --git a/airhub_app/lib/features/auth/domain/entities/user.freezed.dart b/airhub_app/lib/features/auth/domain/entities/user.freezed.dart
index 9ba14f0..2a0c9cb 100644
--- a/airhub_app/lib/features/auth/domain/entities/user.freezed.dart
+++ b/airhub_app/lib/features/auth/domain/entities/user.freezed.dart
@@ -15,7 +15,7 @@ T _$identity(T value) => value;
/// @nodoc
mixin _$User {
- String get id; String get phoneNumber; String? get nickname; String? get avatarUrl;
+ int get id; String get phone; String? get nickname; String? get avatar; String? get gender; String? get birthday;
/// Create a copy of User
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $UserCopyWith get copyWith => _$UserCopyWithImpl(this as User, _$ide
@override
bool operator ==(Object other) {
- return identical(this, other) || (other.runtimeType == runtimeType&&other is User&&(identical(other.id, id) || other.id == id)&&(identical(other.phoneNumber, phoneNumber) || other.phoneNumber == phoneNumber)&&(identical(other.nickname, nickname) || other.nickname == nickname)&&(identical(other.avatarUrl, avatarUrl) || other.avatarUrl == avatarUrl));
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is User&&(identical(other.id, id) || other.id == id)&&(identical(other.phone, phone) || other.phone == phone)&&(identical(other.nickname, nickname) || other.nickname == nickname)&&(identical(other.avatar, avatar) || other.avatar == avatar)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.birthday, birthday) || other.birthday == birthday));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
-int get hashCode => Object.hash(runtimeType,id,phoneNumber,nickname,avatarUrl);
+int get hashCode => Object.hash(runtimeType,id,phone,nickname,avatar,gender,birthday);
@override
String toString() {
- return 'User(id: $id, phoneNumber: $phoneNumber, nickname: $nickname, avatarUrl: $avatarUrl)';
+ return 'User(id: $id, phone: $phone, nickname: $nickname, avatar: $avatar, gender: $gender, birthday: $birthday)';
}
@@ -48,7 +48,7 @@ abstract mixin class $UserCopyWith<$Res> {
factory $UserCopyWith(User value, $Res Function(User) _then) = _$UserCopyWithImpl;
@useResult
$Res call({
- String id, String phoneNumber, String? nickname, String? avatarUrl
+ int id, String phone, String? nickname, String? avatar, String? gender, String? birthday
});
@@ -65,12 +65,14 @@ class _$UserCopyWithImpl<$Res>
/// Create a copy of User
/// with the given fields replaced by the non-null parameter values.
-@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? phoneNumber = null,Object? nickname = freezed,Object? avatarUrl = freezed,}) {
+@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? phone = null,Object? nickname = freezed,Object? avatar = freezed,Object? gender = freezed,Object? birthday = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
-as String,phoneNumber: null == phoneNumber ? _self.phoneNumber : phoneNumber // ignore: cast_nullable_to_non_nullable
+as int,phone: null == phone ? _self.phone : phone // ignore: cast_nullable_to_non_nullable
as String,nickname: freezed == nickname ? _self.nickname : nickname // ignore: cast_nullable_to_non_nullable
-as String?,avatarUrl: freezed == avatarUrl ? _self.avatarUrl : avatarUrl // ignore: cast_nullable_to_non_nullable
+as String?,avatar: freezed == avatar ? _self.avatar : avatar // ignore: cast_nullable_to_non_nullable
+as String?,gender: freezed == gender ? _self.gender : gender // ignore: cast_nullable_to_non_nullable
+as String?,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable
as String?,
));
}
@@ -156,10 +158,10 @@ return $default(_that);case _:
/// }
/// ```
-@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String phoneNumber, String? nickname, String? avatarUrl)? $default,{required TResult orElse(),}) {final _that = this;
+@optionalTypeArgs TResult maybeWhen(TResult Function( int id, String phone, String? nickname, String? avatar, String? gender, String? birthday)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _User() when $default != null:
-return $default(_that.id,_that.phoneNumber,_that.nickname,_that.avatarUrl);case _:
+return $default(_that.id,_that.phone,_that.nickname,_that.avatar,_that.gender,_that.birthday);case _:
return orElse();
}
@@ -177,10 +179,10 @@ return $default(_that.id,_that.phoneNumber,_that.nickname,_that.avatarUrl);case
/// }
/// ```
-@optionalTypeArgs TResult when(TResult Function( String id, String phoneNumber, String? nickname, String? avatarUrl) $default,) {final _that = this;
+@optionalTypeArgs TResult when(TResult Function( int id, String phone, String? nickname, String? avatar, String? gender, String? birthday) $default,) {final _that = this;
switch (_that) {
case _User():
-return $default(_that.id,_that.phoneNumber,_that.nickname,_that.avatarUrl);case _:
+return $default(_that.id,_that.phone,_that.nickname,_that.avatar,_that.gender,_that.birthday);case _:
throw StateError('Unexpected subclass');
}
@@ -197,10 +199,10 @@ return $default(_that.id,_that.phoneNumber,_that.nickname,_that.avatarUrl);case
/// }
/// ```
-@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String phoneNumber, String? nickname, String? avatarUrl)? $default,) {final _that = this;
+@optionalTypeArgs TResult? whenOrNull(TResult? Function( int id, String phone, String? nickname, String? avatar, String? gender, String? birthday)? $default,) {final _that = this;
switch (_that) {
case _User() when $default != null:
-return $default(_that.id,_that.phoneNumber,_that.nickname,_that.avatarUrl);case _:
+return $default(_that.id,_that.phone,_that.nickname,_that.avatar,_that.gender,_that.birthday);case _:
return null;
}
@@ -212,13 +214,15 @@ return $default(_that.id,_that.phoneNumber,_that.nickname,_that.avatarUrl);case
@JsonSerializable()
class _User implements User {
- const _User({required this.id, required this.phoneNumber, this.nickname, this.avatarUrl});
+ const _User({required this.id, required this.phone, this.nickname, this.avatar, this.gender, this.birthday});
factory _User.fromJson(Map json) => _$UserFromJson(json);
-@override final String id;
-@override final String phoneNumber;
+@override final int id;
+@override final String phone;
@override final String? nickname;
-@override final String? avatarUrl;
+@override final String? avatar;
+@override final String? gender;
+@override final String? birthday;
/// Create a copy of User
/// with the given fields replaced by the non-null parameter values.
@@ -233,16 +237,16 @@ Map toJson() {
@override
bool operator ==(Object other) {
- return identical(this, other) || (other.runtimeType == runtimeType&&other is _User&&(identical(other.id, id) || other.id == id)&&(identical(other.phoneNumber, phoneNumber) || other.phoneNumber == phoneNumber)&&(identical(other.nickname, nickname) || other.nickname == nickname)&&(identical(other.avatarUrl, avatarUrl) || other.avatarUrl == avatarUrl));
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is _User&&(identical(other.id, id) || other.id == id)&&(identical(other.phone, phone) || other.phone == phone)&&(identical(other.nickname, nickname) || other.nickname == nickname)&&(identical(other.avatar, avatar) || other.avatar == avatar)&&(identical(other.gender, gender) || other.gender == gender)&&(identical(other.birthday, birthday) || other.birthday == birthday));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
-int get hashCode => Object.hash(runtimeType,id,phoneNumber,nickname,avatarUrl);
+int get hashCode => Object.hash(runtimeType,id,phone,nickname,avatar,gender,birthday);
@override
String toString() {
- return 'User(id: $id, phoneNumber: $phoneNumber, nickname: $nickname, avatarUrl: $avatarUrl)';
+ return 'User(id: $id, phone: $phone, nickname: $nickname, avatar: $avatar, gender: $gender, birthday: $birthday)';
}
@@ -253,7 +257,7 @@ abstract mixin class _$UserCopyWith<$Res> implements $UserCopyWith<$Res> {
factory _$UserCopyWith(_User value, $Res Function(_User) _then) = __$UserCopyWithImpl;
@override @useResult
$Res call({
- String id, String phoneNumber, String? nickname, String? avatarUrl
+ int id, String phone, String? nickname, String? avatar, String? gender, String? birthday
});
@@ -270,12 +274,14 @@ class __$UserCopyWithImpl<$Res>
/// Create a copy of User
/// with the given fields replaced by the non-null parameter values.
-@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? phoneNumber = null,Object? nickname = freezed,Object? avatarUrl = freezed,}) {
+@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? phone = null,Object? nickname = freezed,Object? avatar = freezed,Object? gender = freezed,Object? birthday = freezed,}) {
return _then(_User(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
-as String,phoneNumber: null == phoneNumber ? _self.phoneNumber : phoneNumber // ignore: cast_nullable_to_non_nullable
+as int,phone: null == phone ? _self.phone : phone // ignore: cast_nullable_to_non_nullable
as String,nickname: freezed == nickname ? _self.nickname : nickname // ignore: cast_nullable_to_non_nullable
-as String?,avatarUrl: freezed == avatarUrl ? _self.avatarUrl : avatarUrl // ignore: cast_nullable_to_non_nullable
+as String?,avatar: freezed == avatar ? _self.avatar : avatar // ignore: cast_nullable_to_non_nullable
+as String?,gender: freezed == gender ? _self.gender : gender // ignore: cast_nullable_to_non_nullable
+as String?,birthday: freezed == birthday ? _self.birthday : birthday // ignore: cast_nullable_to_non_nullable
as String?,
));
}
diff --git a/airhub_app/lib/features/auth/domain/entities/user.g.dart b/airhub_app/lib/features/auth/domain/entities/user.g.dart
index 5306719..9de667e 100644
--- a/airhub_app/lib/features/auth/domain/entities/user.g.dart
+++ b/airhub_app/lib/features/auth/domain/entities/user.g.dart
@@ -7,15 +7,19 @@ part of 'user.dart';
// **************************************************************************
_User _$UserFromJson(Map json) => _User(
- id: json['id'] as String,
- phoneNumber: json['phoneNumber'] as String,
+ id: (json['id'] as num).toInt(),
+ phone: json['phone'] as String,
nickname: json['nickname'] as String?,
- avatarUrl: json['avatarUrl'] as String?,
+ avatar: json['avatar'] as String?,
+ gender: json['gender'] as String?,
+ birthday: json['birthday'] as String?,
);
Map _$UserToJson(_User instance) => {
'id': instance.id,
- 'phoneNumber': instance.phoneNumber,
+ 'phone': instance.phone,
'nickname': instance.nickname,
- 'avatarUrl': instance.avatarUrl,
+ 'avatar': instance.avatar,
+ 'gender': instance.gender,
+ 'birthday': instance.birthday,
};
diff --git a/airhub_app/lib/features/auth/domain/repositories/auth_repository.dart b/airhub_app/lib/features/auth/domain/repositories/auth_repository.dart
index e647e75..b298bba 100644
--- a/airhub_app/lib/features/auth/domain/repositories/auth_repository.dart
+++ b/airhub_app/lib/features/auth/domain/repositories/auth_repository.dart
@@ -3,8 +3,11 @@ import '../../../../core/errors/failures.dart';
import '../entities/user.dart';
abstract class AuthRepository {
- Future> loginWithPhone(String phoneNumber, String code);
- Future> oneClickLogin();
+ Future> sendCode(String phone);
+ Future> codeLogin(String phone, String code);
+ Future> tokenLogin(String token);
Future> logout();
+ Future> deleteAccount();
Stream get authStateChanges;
+ User? get currentUser;
}
diff --git a/airhub_app/lib/features/auth/presentation/controllers/auth_controller.dart b/airhub_app/lib/features/auth/presentation/controllers/auth_controller.dart
index d91392c..618866b 100644
--- a/airhub_app/lib/features/auth/presentation/controllers/auth_controller.dart
+++ b/airhub_app/lib/features/auth/presentation/controllers/auth_controller.dart
@@ -10,23 +10,71 @@ class AuthController extends _$AuthController {
// Initial state is void (idle)
}
- Future loginWithPhone(String phoneNumber, String code) async {
+ Future sendCode(String phone) async {
state = const AsyncLoading();
final repository = ref.read(authRepositoryProvider);
- final result = await repository.loginWithPhone(phoneNumber, code);
+ final result = await repository.sendCode(phone);
state = result.fold(
(failure) => AsyncError(failure.message, StackTrace.current),
- (user) => const AsyncData(null),
+ (_) => const AsyncData(null),
);
}
- Future oneClickLogin() async {
+ Future codeLogin(String phone, String code) async {
state = const AsyncLoading();
final repository = ref.read(authRepositoryProvider);
- final result = await repository.oneClickLogin();
+ final result = await repository.codeLogin(phone, code);
+ return result.fold(
+ (failure) {
+ state = AsyncError(failure.message, StackTrace.current);
+ return false;
+ },
+ (user) {
+ state = const AsyncData(null);
+ return true;
+ },
+ );
+ }
+
+ Future tokenLogin(String token) async {
+ state = const AsyncLoading();
+ final repository = ref.read(authRepositoryProvider);
+ final result = await repository.tokenLogin(token);
+ return result.fold(
+ (failure) {
+ state = AsyncError(failure.message, StackTrace.current);
+ return false;
+ },
+ (user) {
+ state = const AsyncData(null);
+ return true;
+ },
+ );
+ }
+
+ Future logout() async {
+ state = const AsyncLoading();
+ final repository = ref.read(authRepositoryProvider);
+ final result = await repository.logout();
state = result.fold(
(failure) => AsyncError(failure.message, StackTrace.current),
- (user) => const AsyncData(null),
+ (_) => const AsyncData(null),
+ );
+ }
+
+ Future deleteAccount() async {
+ state = const AsyncLoading();
+ final repository = ref.read(authRepositoryProvider);
+ final result = await repository.deleteAccount();
+ return result.fold(
+ (failure) {
+ state = AsyncError(failure.message, StackTrace.current);
+ return false;
+ },
+ (_) {
+ state = const AsyncData(null);
+ return true;
+ },
);
}
}
diff --git a/airhub_app/lib/features/auth/presentation/controllers/auth_controller.g.dart b/airhub_app/lib/features/auth/presentation/controllers/auth_controller.g.dart
index c3d053b..fba6b23 100644
--- a/airhub_app/lib/features/auth/presentation/controllers/auth_controller.g.dart
+++ b/airhub_app/lib/features/auth/presentation/controllers/auth_controller.g.dart
@@ -33,7 +33,7 @@ final class AuthControllerProvider
AuthController create() => AuthController();
}
-String _$authControllerHash() => r'e7278df3deb6222da1e5e8b8b6ec921493441758';
+String _$authControllerHash() => r'fac48518f4825055a266b5ea7e11163320342153';
abstract class _$AuthController extends $AsyncNotifier {
FutureOr build();
diff --git a/airhub_app/lib/features/auth/presentation/pages/login_page.dart b/airhub_app/lib/features/auth/presentation/pages/login_page.dart
index 4548cba..ab1bda4 100644
--- a/airhub_app/lib/features/auth/presentation/pages/login_page.dart
+++ b/airhub_app/lib/features/auth/presentation/pages/login_page.dart
@@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
+import '../../../../core/services/phone_auth_service.dart';
import '../../../../theme/app_colors.dart';
import '../../../../widgets/animated_gradient_background.dart';
import '../../../../widgets/gradient_button.dart';
@@ -49,17 +50,11 @@ class _LoginPageState extends ConsumerState {
super.dispose();
}
- void _handleListener(BuildContext context, AsyncValue next) {
+ void _handleListener(BuildContext context, AsyncValue? prev, AsyncValue next) {
next.whenOrNull(
error: (error, stack) {
_showToast(error.toString(), isError: true);
},
- data: (_) {
- // Navigate to Home on success
- if (mounted) {
- context.go('/home');
- }
- },
);
}
@@ -209,8 +204,19 @@ class _LoginPageState extends ConsumerState {
}
// Logic Methods
- void _doOneClickLogin() {
- ref.read(authControllerProvider.notifier).oneClickLogin();
+ Future _doOneClickLogin() async {
+ // 通过阿里云号码认证 SDK 获取 token
+ final phoneAuthService = ref.read(phoneAuthServiceProvider);
+ final token = await phoneAuthService.getLoginToken();
+ if (token == null) {
+ if (mounted) _showToast('一键登录取消或失败,请使用验证码登录', isError: true);
+ return;
+ }
+ if (!mounted) return;
+ final success = await ref.read(authControllerProvider.notifier).tokenLogin(token);
+ if (success && mounted) {
+ context.go('/home');
+ }
}
void _handleOneClickLogin() {
@@ -234,28 +240,37 @@ class _LoginPageState extends ConsumerState {
AppToast.show(context, message, isError: isError);
}
- void _sendCode() {
+ Future _sendCode() async {
if (!_isValidPhone(_phoneController.text)) {
_showToast('请输入正确的手机号', isError: true);
return;
}
- setState(() => _countdown = 60);
- _showToast('验证码已发送');
- _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
- if (_countdown <= 1) {
- timer.cancel();
- if (mounted) setState(() => _countdown = 0);
- } else {
- if (mounted) setState(() => _countdown--);
- }
- });
+ try {
+ await ref.read(authControllerProvider.notifier).sendCode(_phoneController.text);
+ if (!mounted) return;
+ setState(() => _countdown = 60);
+ _showToast('验证码已发送');
+ _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
+ if (_countdown <= 1) {
+ timer.cancel();
+ if (mounted) setState(() => _countdown = 0);
+ } else {
+ if (mounted) setState(() => _countdown--);
+ }
+ });
+ } catch (_) {
+ // 错误已通过 listener 处理
+ }
}
- void _submitSmsLogin() {
+ Future _submitSmsLogin() async {
if (!_canSubmitSms) return;
- ref
+ final success = await ref
.read(authControllerProvider.notifier)
- .loginWithPhone(_phoneController.text, _codeController.text);
+ .codeLogin(_phoneController.text, _codeController.text);
+ if (success && mounted) {
+ context.go('/home');
+ }
}
Widget _buildGradientBackground() {
@@ -267,7 +282,7 @@ class _LoginPageState extends ConsumerState {
// Listen to Auth State
ref.listen(
authControllerProvider,
- (_, next) => _handleListener(context, next),
+ (prev, next) => _handleListener(context, prev, next),
);
final isLoading = ref.watch(authControllerProvider).isLoading;
diff --git a/airhub_app/lib/features/device/data/datasources/device_remote_data_source.dart b/airhub_app/lib/features/device/data/datasources/device_remote_data_source.dart
new file mode 100644
index 0000000..95bed06
--- /dev/null
+++ b/airhub_app/lib/features/device/data/datasources/device_remote_data_source.dart
@@ -0,0 +1,115 @@
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+import '../../../../core/network/api_client.dart';
+import '../../domain/entities/device.dart';
+import '../../domain/entities/device_detail.dart';
+
+part 'device_remote_data_source.g.dart';
+
+abstract class DeviceRemoteDataSource {
+ /// GET /devices/query-by-mac/?mac=xxx (无需认证)
+ Future