docs(recent-calls): 架构文档补充(#42~#44)
新增 Doc/recent_calls_architecture.md: - 功能范围、目录结构、数据流图 - API 端点 /app/api/call/records 字段映射表 - 通话状态枚举(0-6)与未接来电判断逻辑 - ViewModel 设计、UI 结构、路由说明 - 设计决策(无用户名映射、全量拉取、离线可用) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
166
Doc/recent_calls_architecture.md
Normal file
166
Doc/recent_calls_architecture.md
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
# 最近呼叫(Recent Calls)— 架构文档
|
||||||
|
|
||||||
|
> 对应 Gitea issues #46-#48(内部编号 #42-#44)
|
||||||
|
> 参考实现:`im-client-im-dev` lib/views/call_log/ + lib/managers/call_log_mgr.dart
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 功能范围
|
||||||
|
|
||||||
|
| Issue | 功能 | 状态 |
|
||||||
|
|-------|------|------|
|
||||||
|
| #42 | 最近呼叫列表页 UI(全部/未接 Tab + CallLogTile) | ✅ 已实现 |
|
||||||
|
| #43 | 通话记录 API 接入(FetchCallLogsRequest + UseCase) | ✅ 已实现 |
|
||||||
|
| #44 | RecentCallsViewModel + 路由接入 | ✅ 已实现 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
features/settings/
|
||||||
|
├── di/
|
||||||
|
│ └── settings_providers.dart # 新增 fetchCallLogsUseCaseProvider
|
||||||
|
├── presentation/
|
||||||
|
│ └── recent_calls_view_model.dart # RecentCallsViewModel + RecentCallsState
|
||||||
|
├── usecases/
|
||||||
|
│ └── fetch_call_logs_usecase.dart # POST /app/api/call/records → DB
|
||||||
|
└── view/
|
||||||
|
└── recent_calls_page.dart # RecentCallsPage + TabBar + _CallLogTile
|
||||||
|
|
||||||
|
data/remote/
|
||||||
|
└── call_log_request.dart # FetchCallLogsRequest + CallLogDto
|
||||||
|
|
||||||
|
app/router/
|
||||||
|
├── app_route_name.dart # 新增 settingsRecentCalls
|
||||||
|
├── app_router.dart # 新增 GoRoute
|
||||||
|
└── guards/auth_guard.dart # 新增 case settingsRecentCalls
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 数据流
|
||||||
|
|
||||||
|
```
|
||||||
|
RecentCallsPage (build)
|
||||||
|
└─ ref.watch(allCallLogsProvider) ← DB Stream(实时)
|
||||||
|
└─ RecentCallsViewModel.loadCallLogs()
|
||||||
|
└─ FetchCallLogsUseCase.execute()
|
||||||
|
└─ POST /app/api/call/records (start_from=0, status=-1)
|
||||||
|
├─ 响应 List<CallLogDto> → CallLog Domain
|
||||||
|
└─ CallLogRepository.insertOrReplaceCallLogs()
|
||||||
|
└─ Drift DB → allCallLogsProvider 自动刷新 → UI
|
||||||
|
|
||||||
|
进入页面:
|
||||||
|
└─ RecentCallsViewModel.markAllRead()
|
||||||
|
└─ CallLogRepository.markAllAsRead()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. API 端点
|
||||||
|
|
||||||
|
| 操作 | Method | Path | 参数 |
|
||||||
|
|------|--------|------|------|
|
||||||
|
| 拉取通话记录 | POST | `/app/api/call/records` | `start_from` (int, 时间戳), `status` (-1=全部) |
|
||||||
|
|
||||||
|
### 响应字段映射
|
||||||
|
|
||||||
|
| 服务端字段 | Dart 字段 | 类型 |
|
||||||
|
|-----------|-----------|------|
|
||||||
|
| `rtc_channel_id` | `id` (String) | String |
|
||||||
|
| `inviter_id` / `caller_id` | `callerId` | int? |
|
||||||
|
| `receiver_id` | `receiverId` | int? |
|
||||||
|
| `chat_id` | `chatId` | int? |
|
||||||
|
| `duration` | `duration` | int? (秒) |
|
||||||
|
| `video_call` | `videoCall` | int? (0=语音, 1=视频) |
|
||||||
|
| `created_at` | `createdAt` | int? (秒级时间戳) |
|
||||||
|
| `updated_at` | `updatedAt` | int? |
|
||||||
|
| `ended_at` | `endedAt` | int? |
|
||||||
|
| `status` | `status` | int? |
|
||||||
|
| `is_read` | `isRead` | int? (0/1) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 通话状态枚举
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// status 值含义(参考 im-client-im-dev CallEvent)
|
||||||
|
// 0 = Ringing/Pending
|
||||||
|
// 1 = Connected
|
||||||
|
// 2 = Ended(正常结束)
|
||||||
|
// 3 = Busy(CallOptBusy)
|
||||||
|
// 4 = Cancelled(CallOptCancel)
|
||||||
|
// 5 = Timeout(CallTimeOut)
|
||||||
|
// 6 = Declined(CallBusy)
|
||||||
|
|
||||||
|
// 未接来电:callerId != currentUid AND status in {3, 4, 5, 6}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. ViewModel 设计
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class RecentCallsState {
|
||||||
|
final int tabIndex; // 0=全部, 1=未接来电
|
||||||
|
final bool isLoading;
|
||||||
|
final String? error;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecentCallsViewModel extends Notifier<RecentCallsState> {
|
||||||
|
void setTab(int index) // 切换 Tab
|
||||||
|
Future<void> loadCallLogs() // 拉取 + 写入 DB
|
||||||
|
Future<void> markAllRead() // 标记已读
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. UI 结构
|
||||||
|
|
||||||
|
### RecentCallsPage
|
||||||
|
|
||||||
|
```
|
||||||
|
Scaffold
|
||||||
|
AppBar: 「最近呼叫」
|
||||||
|
Column
|
||||||
|
TabBar(全部 / 未接来电)
|
||||||
|
Expanded
|
||||||
|
TabBarView
|
||||||
|
AllCallsTab → allCallLogsProvider 全部
|
||||||
|
MissedTab → 按 isMissed 过滤
|
||||||
|
```
|
||||||
|
|
||||||
|
### _CallLogTile
|
||||||
|
|
||||||
|
```
|
||||||
|
ListTile
|
||||||
|
leading: _CallTypeIcon(电话/摄像头/红色, 40pt 圆形)
|
||||||
|
title: 对方 UID 显示("@J{uid}")
|
||||||
|
subtitle: 通话类型文字 + 时长
|
||||||
|
trailing: 相对时间
|
||||||
|
```
|
||||||
|
|
||||||
|
**未接来电判断**:`callerId != currentUid` AND `status in {3,4,5,6}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 路由
|
||||||
|
|
||||||
|
```
|
||||||
|
/settings/recent-calls → RecentCallsPage(parentNavigatorKey=_rootKey)
|
||||||
|
```
|
||||||
|
|
||||||
|
`auth_guard.dart` switch 已补充 `settingsRecentCalls` case。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 设计决策
|
||||||
|
|
||||||
|
- **无用户名映射**:当前 CallLog 实体只有 UID,无昵称。显示 `@J{uid}` 占位,
|
||||||
|
后续可接入联系人 Repository 做名称查询。
|
||||||
|
- **增量拉取策略**:当前实现 `start_from=0`(全量),首次打开可能慢;
|
||||||
|
后续可从本地最新 `updatedAt` 做增量同步(参考 CallLogMgr.loadRemoteCallLog)。
|
||||||
|
- **离线可用**:数据先写 DB,UI 监听 `allCallLogsProvider`(DB Stream),
|
||||||
|
无网络时仍能展示缓存记录。
|
||||||
Reference in New Issue
Block a user