# 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 完整注册流程。 ## 调用流程 1. Flutter 调 Google Sign-In / Firebase Auth,拿到 Firebase ID token。 2. Flutter 暂存: - `provider=firebase` - `credential=` - Google 返回的昵称、头像等基础信息 - `device_id`、`platform`、`language`、`timezone` 3. 进入资料页,用户补齐 `username`、`avatar`、`gender`、`country`、`invite_code`。 4. 如果用户选择本地头像,先调用注册头像上传接口拿 URL。 5. 调 `POST /api/v1/auth/third-party/register`。 6. 成功后把返回的 token 写入 `AppAuthSession`。 ## 注册头像上传 地址: ```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": 1800, "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 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; 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 导入接口。