# 畅联 · 手机端（Flutter）

公司内部通讯 App 的手机端源码。骨架配色走企业微信式办公风（主色蓝 `#3B87F5`），聊天手感照微信。仓库已带 `android/`、`ios/` 平台脚手架与全部权限声明，**不需要再跑 `flutter create`**。

## 打包 / 运行

打包是开发机上的事，**服务器上不需要装 Flutter**（服务器只跑 docker 后端）。在装好 Flutter 与 Android SDK 的开发机上：

```bash
cd mobile
flutter pub get
flutter build apk --release   # 出安卓安装包 build/app/outputs/flutter-apk/app-release.apk
```

iOS 需要一台 Mac 搭配 Xcode 与开发者证书，另按苹果流程打包。

已声明的平台权限（勿删）：

- `android/app/src/main/AndroidManifest.xml`：`INTERNET`、`RECORD_AUDIO`（语音消息/通话）、`MODIFY_AUDIO_SETTINGS`、存储/相册读取（发文件、发图片）；并开启了 `usesCleartextTraffic`（内网服务器暂用 http/ws 明文，开通外网改 HTTPS 后应移除）。
- `ios/Runner/Info.plist`：`NSMicrophoneUsageDescription`（语音消息与通话）、`NSPhotoLibraryUsageDescription`（发图片）、`NSAppTransportSecurity/NSAllowsArbitraryLoads`（同上，内网明文）。
- `android/build.gradle.kts` / `settings.gradle.kts` 把阿里云 Maven 镜像放在最前（本网络访问 maven.google.com 不稳定）。

## 修改服务器地址

所有服务器地址集中在 `lib/config.dart`，改 `serverHost`（内网 IP/域名）和对应端口常量即可：

| 常量 | 用途 | 默认值 |
| --- | --- | --- |
| `apiAddr` | OpenIM API | `http://192.168.200.11:10002` |
| `wsAddr` | OpenIM WebSocket | `ws://192.168.200.11:10001` |
| `livekitUrl` | LiveKit 语音通话 | `ws://192.168.200.11:17880`（注意已重映射，非默认 7880） |
| `authApiBase` | 公司账号登录后端 | `http://192.168.200.11:10010` |

## 登录接口契约（后端：任务 B-60 account-service，已上线）

### 1. 账号登录

```
POST {authApiBase}/api/login
Content-Type: application/json

{"staffNo": "工号", "password": "...", "platformID": 1}
```

- `platformID`：1 = iOS，2 = Android。
- 响应统一包络 `{"code": 0, "msg": "", "data": {...}}`（业务失败也是 HTTP 200 + `code != 0`，HTTP 401/400 时同样带 `msg`）。
- 成功 `data`：`{"userID": "...", "nickname": "...", "imToken": "...", "expireTimeSeconds": ...}`
  - `userID` / `imToken` 直接传给 OpenIM SDK 的 `login`（OpenIM 的登录 token 由后端用 OpenIM 管理端接口换取）。
- 失败：`msg` 会原样展示给用户。

App 登录流程：`AuthApi.login` → 拿到 userID/imToken → `IMService.login`（SDK `initSDK` 在 App 启动时已执行一次）→ 进主页。userID/imToken 存 `shared_preferences`，下次启动自动登录；SDK 回调 `onUserTokenExpired` / `onKickedOffline` 时清凭证回登录页。

### 2. 语音通话 token

官方样板工程的 LiveKit token 取自 open-im-chat 业务服务端（`/user/rtc/get_token`，端口 10008），本项目服务端裁剪时移除了 open-im-chat，由 account-service 提供等价接口：

```
POST {authApiBase}/api/rtc_token
Authorization: Bearer {登录返回的 imToken}
Content-Type: application/json

{"room": "房间号", "identity": "当前用户 userID"}
```

- 成功 `data`：`{"token": "LiveKit 访问 token"}`（App 使用 `config.dart` 中的 `livekitUrl` 作为连接地址；后端用与 livekit 容器同一对 `LIVEKIT_API_KEY` / `LIVEKIT_API_SECRET` 签发 HS256 JWT）。
- 后端会先拿 `Authorization` 里的 IM token 调 OpenIM `parse_token` 校验身份，且要求 `identity` 与 token 对应的 userID 一致。
- 通话信令（呼叫/接听/拒绝/取消/挂断）走 OpenIM 自定义消息（customType 200-204 的仅在线消息），与官方样板工程协议一致，不需要后端参与。

## 代码结构

```
lib/
  main.dart                 入口：主题、路由、启动页（自动登录）、被踢下线/来电处理
  config.dart               服务器地址集中配置
  theme.dart                颜色、字号阶梯（4 级）、间距常量（4 的倍数）
  models/signaling.dart     通话信令数据模型（与官方样板一致）
  utils/format.dart         时间格式化、文件大小、会话预览文案
  services/
    im_service.dart         OpenIM SDK 封装：init/login/监听/会话列表（ChangeNotifier）
    auth_api.dart           公司账号登录 + LiveKit token 的 HTTP 客户端（dio）
    call_service.dart       一对一语音通话：信令 + LiveKit 房间
  screens/                  登录、主页三 Tab（消息/通讯录/我的）、聊天、通话等页面
  widgets/                  头像、消息气泡、输入栏、四态视图
```

## 测试账号

员工账号由管理员在 account-service 管理页批量导入（工号 + 姓名 + 初始密码），App 端不开放自助注册。联调时请管理员先导入测试名单。
