hyapp-server/docs/flutter对接/Google三方注册Flutter对接.md
2026-07-06 11:10:19 +08:00

304 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Google 三方注册 Flutter 对接
## 边界
Google 登录后Flutter 只暂存 Firebase ID token 和 Google 基本信息,不写入 `AppAuthSession`
资料页允许 pending third-party auth 进入,不要求 `_authService.isSignedIn`。用户点击完成后Flutter 调注册接口;后端校验 Firebase ID token、创建或补齐用户资料、返回 access/refresh token。Flutter 收到 token 后再写入现有 `AppAuthSession`
旧接口 `POST /api/v1/auth/third-party/login` 仍保留兼容。新 Google 流程只能在检查接口返回 `registered=true` 后调用它拿 session不要在 `registered=false` 时调用它,避免旧接口创建 pending 用户。
## 调用流程
1. Flutter 调 Google Sign-In / Firebase Auth拿到 Firebase ID token。
2. Flutter 暂存:
- `provider=firebase`
- `credential=<firebase_id_token>`
- Google 返回的昵称、头像等基础信息
- `device_id``platform``language``timezone`
3. 调注册检查接口。
4. 如果 `registered=true`,调用旧三方登录接口拿 session写入 `AppAuthSession` 后进首页。
5. 如果 `registered=false`,进入资料页,用户补齐 `username``avatar``gender``country``invite_code`
6. 如果用户选择本地头像,先调用注册头像上传接口拿 URL。
7.`POST /api/v1/auth/third-party/register`
8. 成功后把返回的 token 写入 `AppAuthSession`
## 注册检查
地址:
```http
POST /api/v1/auth/third-party/registered/check
```
Headers
| Header | 必填 | 说明 |
| --- | --- | --- |
| `Content-Type` | 是 | `application/json` |
| `X-App-Code` | 否 | App 编码,默认 `lalu` |
| `X-App-Package` | 否 | 包名;后端可按包名解析 app_code。 |
参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `provider` | string | 是 | 固定传 `firebase`。 |
| `credential` | string | 是 | Firebase ID token。 |
请求示例:
```json
{
"provider": "firebase",
"credential": "firebase_id_token"
}
```
返回值:
```json
{
"code": 0,
"message": "ok",
"request_id": "req_xxx",
"data": {
"registered": true
}
}
```
Flutter 处理:
| `registered` | 处理 |
| --- | --- |
| `true` | 用户已完成注册;再调用 `POST /api/v1/auth/third-party/login` 获取 session。 |
| `false` | 用户未完成注册,或者只有旧流程留下的 pending 用户;进入资料页,最后调用注册接口。 |
## 注册头像上传
地址:
```http
POST /api/v1/files/registration/avatar/upload
```
Headers
| Header | 必填 | 说明 |
| --- | --- | --- |
| `Content-Type` | 是 | `multipart/form-data` |
| `X-App-Code` | 否 | App 编码,默认 `lalu` |
参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `file` | file | 是 | 头像文件;也支持字段名 `avatar`。 |
限制:
| 项 | 说明 |
| --- | --- |
| 登录态 | 不需要 `Authorization`。 |
| 大小 | 最大 5MB。 |
| 格式 | `jpg``jpeg``png``webp`。 |
| 校验 | 后端按文件头识别图片类型,不信任客户端传入的 Content-Type。 |
返回值:
```json
{
"code": 0,
"message": "ok",
"request_id": "req_xxx",
"data": {
"url": "https://media.example.com/app/avatars/registration/20260626/avatar_xxx.png",
"object_key": "app/avatars/registration/20260626/avatar_xxx.png",
"content_type": "image/png",
"size_bytes": 204800
}
}
```
Flutter 使用 `data.url` 作为注册接口的 `avatar`
## 三方注册
地址:
```http
POST /api/v1/auth/third-party/register
```
Headers
| Header | 必填 | 说明 |
| --- | --- | --- |
| `Content-Type` | 是 | `application/json` |
| `X-App-Code` | 否 | App 编码,默认 `lalu` |
| `X-App-Package` | 否 | 包名;后端可按包名解析 app_code。 |
参数:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `provider` | string | 是 | 固定传 `firebase`。 |
| `credential` | string | 是 | Firebase ID token。 |
| `device_id` | string | 是 | 设备 ID用于 refresh session 绑定和注册风控。 |
| `username` | string | 是 | 用户昵称。 |
| `avatar` | string | 是 | HTTP(S) 头像 URL可以是上传接口返回的 `url`。 |
| `gender` | string | 是 | 性别值,后端只保存原值。 |
| `country` | string | 是 | 国家码,例如 `SG``CN`。 |
| `invite_code` | string | 否 | 邀请码。 |
| `device` | string | 否 | 设备型号。 |
| `os_version` | string | 否 | 系统版本。 |
| `birth` | string | 否 | 生日,格式 `yyyy-mm-dd`。 |
| `app_version` | string | 否 | App 版本。 |
| `build_number` | string | 否 | 构建号。 |
| `source` | string | 否 | 注册来源。 |
| `install_channel` | string | 否 | 安装渠道。 |
| `campaign` | string | 否 | 投放活动。 |
| `platform` | string | 是 | `android``ios`。 |
| `language` | string | 否 | 客户端语言,例如 `en-US`。 |
| `timezone` | string | 否 | IANA 时区,例如 `Asia/Shanghai`。 |
请求示例:
```json
{
"provider": "firebase",
"credential": "firebase_id_token",
"device_id": "ios_device_001",
"username": "Alice",
"avatar": "https://media.example.com/app/avatars/registration/20260626/avatar_xxx.png",
"gender": "female",
"country": "SG",
"invite_code": "",
"device": "iPhone 15",
"os_version": "iOS 18.1",
"birth": "2000-01-02",
"app_version": "1.2.3",
"build_number": "123",
"source": "google",
"install_channel": "app_store",
"campaign": "",
"platform": "ios",
"language": "en-US",
"timezone": "Asia/Shanghai"
}
```
返回值:
```json
{
"code": 0,
"message": "ok",
"request_id": "req_xxx",
"data": {
"app_code": "lalu",
"user_id": "10001",
"display_user_id": "163001",
"default_display_user_id": "163001",
"display_user_id_kind": "default",
"display_user_id_expires_at_ms": 0,
"session_id": "sess_xxx",
"access_token": "access_token",
"refresh_token": "refresh_token",
"expires_in_sec": 604800,
"token_type": "Bearer",
"is_new_user": true,
"profile_completed": true,
"onboarding_status": "completed"
}
}
```
Flutter 成功处理:
| 字段 | 处理 |
| --- | --- |
| `access_token` | 写入现有 `AppAuthSession.accessToken`。 |
| `refresh_token` | 写入现有 `AppAuthSession.refreshToken`。 |
| `session_id` | 写入现有 session 标识。 |
| `user_id` | 字符串保存,不要转 JS number。 |
| `display_user_id` | 展示号。 |
| `profile_completed` | 必须为 `true`;否则不要进入首页。 |
| `onboarding_status` | 成功应为 `completed`。 |
## 重试和兼容
同一个 Firebase subject 如果之前已经被旧流程创建成 pending 用户,新注册接口会补齐这个用户并返回 token不会再创建第二个用户。
同一个 Firebase subject 如果已经完成注册,再调该接口会按已有用户登录返回 token不会覆盖用户后续改过的资料。
## 错误处理
| HTTP | code | 场景 | Flutter 处理 |
| --- | --- | --- | --- |
| `400` | `INVALID_ARGUMENT` | 缺必填字段、国家格式错误、头像 URL 非 HTTP(S)、生日格式错误。 | 停留资料页,提示用户修正。 |
| `400` | `INVALID_INVITE_CODE` | 邀请码格式错误或不可用。 | 提示邀请码无效,可允许清空后重试。 |
| `401` | `AUTH_FAILED` | Firebase ID token 无效、过期或 provider 不允许。 | 回到 Google 登录重新拿 token。 |
| `429` | `RATE_LIMITED` | 登录注册入口限流。 | 稍后重试。 |
| `409` | `DEVICE_ALREADY_REGISTERED` | 当前设备已达到注册账号上限。 | 提示当前设备无法继续注册。 |
| `502` | `UPSTREAM_ERROR` | user-service 或对象存储不可用。 | 稍后重试。 |
## Dart 调用示例
```dart
Future<AppAuthSession> registerFirebaseUser({
required AppNetworkService network,
required String firebaseIdToken,
required String deviceId,
required String username,
required String avatar,
required String gender,
required String country,
String inviteCode = '',
}) async {
final envelope = await network.postEnvelope(
'/api/v1/auth/third-party/register',
data: {
'provider': 'firebase',
'credential': firebaseIdToken,
'device_id': deviceId,
'username': username,
'avatar': avatar,
'gender': gender,
'country': country,
'invite_code': inviteCode,
'platform': Platform.isIOS ? 'ios' : 'android',
'language': PlatformDispatcher.instance.locale.toLanguageTag(),
'timezone': await currentIanaTimezone(),
},
requiresAuthorization: false,
);
if (envelope.code != 0) {
throw ApiException(envelope.code, envelope.message);
}
final data = envelope.data as Map<String, dynamic>;
if (data['profile_completed'] != true ||
data['onboarding_status'] != 'completed') {
throw StateError('third-party register did not complete profile');
}
return AppAuthSession(
appCode: data['app_code'] as String? ?? '',
userId: data['user_id'] as String? ?? '',
displayUserId: data['display_user_id'] as String? ?? '',
sessionId: data['session_id'] as String? ?? '',
accessToken: data['access_token'] as String? ?? '',
refreshToken: data['refresh_token'] as String? ?? '',
expiresInSec: (data['expires_in_sec'] as num? ?? 0).toInt(),
tokenType: data['token_type'] as String? ?? 'Bearer',
profileCompleted: data['profile_completed'] == true,
onboardingStatus: data['onboarding_status'] as String? ?? '',
);
}
```
相关 IM注册接口成功后后端会导入腾讯 IM 用户资料Flutter 不需要额外调用 IM 导入接口。