# Implementation Plan: App启动时语言和时区自动设置 ## Phase 1: 前端 - 读取系统语言/时区 ### 1.1 创建系统Locale工具函数 **文件**: `apps/lib/app/locale_utils.dart` (新建) **Locale映射规则** (基于项目现有约定): | 系统Locale | Flutter Locale | 存储Tag | |-----------|----------------|---------| | en, en-US, en-GB, ... | `Locale('en')` | `en-US` | | zh, zh-CN, zh-SG, zh-Hans-* | `Locale('zh')` | `zh-CN` | | zh-Hant, zh-TW, zh-HK, zh-MO | `Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant')` | `zh-Hant` | | 其他 | null (回退默认) | `zh-CN` | ```dart import 'dart:ui'; import 'package:flutter/material.dart'; /// 从系统Locale映射到App支持的Locale /// 返回 null 表示不支持,调用方应使用默认值 Locale? resolveSystemLocale(Locale systemLocale) { final lang = systemLocale.languageCode.toLowerCase(); final script = systemLocale.scriptCode; final country = systemLocale.countryCode; // 英文: en, en-US, en-GB, ... → Locale('en') → 存储 en-US if (lang == 'en') { return const Locale('en'); } // 中文处理 if (lang == 'zh') { // 繁体: zh-Hant, zh-TW, zh-HK, zh-MO → Locale(zh, Hant) → 存储 zh-Hant if (script == 'Hant' || country == 'TW' || country == 'HK' || country == 'MO') { return const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'); } // 简体: zh, zh-CN, zh-SG, zh-Hans-* → Locale('zh') → 存储 zh-CN return const Locale('zh'); } // 其他语言不支持,回退到默认 return null; } /// 获取系统首选Locale Locale getSystemLocale() { return PlatformDispatcher.instance.locale; } ``` ### 1.2 创建系统时区工具函数 **文件**: `apps/lib/app/timezone_utils.dart` (新建) **方案选择**: - 方案1: 使用 `flutter_timezone` 包 (推荐,直接返回IANA ID) - 方案2: 使用 `DateTime.now().timeZoneName` + 映射表 (无额外依赖,但需维护映射) ```dart import 'package:flutter_timezone/flutter_timezone.dart'; /// 获取系统时区IANA ID /// 例如: Asia/Shanghai, America/New_York, Europe/London Future getSystemTimezone() async { try { final timezone = await FlutterTimezone.getLocalTimezone(); // 验证是否为有效IANA ID if (timezone.isNotEmpty) { return timezone; } } catch (_) { // ignore } // 回退到默认 return 'Asia/Shanghai'; } ``` **依赖添加** (`apps/pubspec.yaml`): ```yaml dependencies: flutter_timezone: ^3.0.1 ``` ### 1.3 扩展SessionStore **文件**: `apps/lib/core/auth/session_store.dart` ```dart // 新增 static const String _timezoneKey = 'selected_timezone'; Future saveTimezone(String timezone) async { await _kvStore.setString(_timezoneKey, timezone); } Future getTimezone() async { return _kvStore.getString(_timezoneKey); } ``` ### 1.4 修改App启动流程 **文件**: `apps/lib/app/app.dart` **步骤1**: 添加 `_timezone` 状态变量声明 ```dart // 在 State 类顶部添加 String _timezone = 'Asia/Shanghai'; ``` **步骤2**: 修改 `_bootstrap()` 方法 ```dart Future _bootstrap() async { // 1. 语言处理 final savedLocaleTag = await _sessionStore.getLocaleTag(); final Locale locale; if (savedLocaleTag != null) { locale = localeFromLanguageTag(savedLocaleTag); } else { final systemLocale = getSystemLocale(); locale = resolveSystemLocale(systemLocale) ?? const Locale('zh'); await _sessionStore.saveLocaleTag(languageTagFromLocale(locale)); } // 2. 时区处理 final savedTimezone = await _sessionStore.getTimezone(); final String timezone; if (savedTimezone != null) { timezone = savedTimezone; } else { timezone = await getSystemTimezone(); await _sessionStore.saveTimezone(timezone); } // 3. 设置状态 setState(() { _locale = locale; _timezone = timezone; }); // 4. 启动认证 await _authBloc.start(); } ``` --- ## Phase 2: 前端 - 注册时传递语言/时区 ### 2.1 扩展AuthApi **文件**: `apps/lib/features/auth/data/apis/auth_api.dart` ```dart Future createEmailSession({ required String email, required String token, String? language, String? timezone, }) async { final data = { 'email': email, 'token': token, }; if (language != null) data['language'] = language; if (timezone != null) data['timezone'] = timezone; final json = await _apiClient.postJson('/api/v1/auth/email-session', data: data); return SessionResponse.fromJson(json); } ``` ### 2.2 扩展AuthRepository **文件**: `apps/lib/features/auth/data/repositories/auth_repository.dart` ```dart // 接口定义 abstract class AuthRepository { // ... 现有方法 ... Future loginWithEmailOtp({ required String email, required String otp, String? language, String? timezone, }); } // 实现 class AuthRepositoryImpl implements AuthRepository { // ... 现有代码 ... @override Future loginWithEmailOtp({ required String email, required String otp, String? language, String? timezone, }) async { final response = await _authApi.createEmailSession( email: email, token: otp, language: language, timezone: timezone, ); // ... 现有的 session 处理逻辑 ... } } ``` ### 2.3 扩展AuthBloc **文件**: `apps/lib/features/auth/presentation/bloc/auth_bloc.dart` ```dart Future loginWithOtp({ required String email, required String otp, String? language, String? timezone, }) async { final user = await _repository.loginWithEmailOtp( email: email, otp: otp, language: language, timezone: timezone, ); // ... } ``` ### 2.4 修改App调用点 **文件**: `apps/lib/app/app.dart` ```dart // LoginScreen 的 onLoginWithOtp 回调 onLoginWithOtp: (email, otp) { return _authBloc.loginWithOtp( email: email, otp: otp, language: languageTagFromLocale(_locale), timezone: _timezone, ); }, ``` --- ## Phase 3: 后端 - 接收语言/时区 ### 3.1 扩展Schema **文件**: `backend/src/v1/auth/schemas.py` ```python class EmailSessionCreateRequest(BaseModel): model_config = ConfigDict(extra="forbid") email: str = Field(pattern=SUPABASE_EMAIL_PATTERN) token: str = Field(min_length=6, max_length=6) language: str | None = Field(default=None, max_length=20) timezone: str | None = Field(default=None, max_length=50) ``` ### 3.2 扩展AuthService **文件**: `backend/src/v1/auth/service.py` ```python async def create_email_session( self, payload: EmailSessionCreateRequest, ) -> SessionResponse: # ... 现有的 session 创建逻辑 ... # 如果提供了语言/时区,更新Profile if payload.language or payload.timezone: await self._update_profile_preferences( user_id=user.id, language=payload.language, timezone=payload.timezone, ) return result async def _update_profile_preferences( self, user_id: UUID, language: str | None, timezone: str | None, ) -> None: """更新用户Profile的语言/时区偏好设置""" profile = await self.profile_repository.get_profile_by_user_id(user_id) if profile is None: return settings = profile.settings or {} preferences = settings.get("preferences", {}) if language is not None: preferences["language"] = language if timezone is not None: preferences["timezone"] = timezone settings["preferences"] = preferences profile.settings = settings await self.profile_repository.save() ``` ### 3.3 AuthService依赖注入 **文件**: `backend/src/v1/auth/service.py` AuthService 需要注入 `ProfileRepository` 或 `UserRepository` 以便更新 Profile。 ```python @dataclass class AuthService: # ... 现有依赖 ... profile_repository: SQLAlchemyUserRepository # 新增 ``` **或**: 复用现有的 `v1/users/service.py` 中的 `UserService.update_settings()` 方法。 --- ## Phase 4: 前端 - 登录后同步时区 ### 4.1 修改_refreshProfile **文件**: `apps/lib/app/app.dart` ```dart Future _refreshProfile({required String userEmail}) async { // ... 现有逻辑 ... final serverLanguage = profile.preferences.language; final serverTimezone = profile.preferences.timezone; // 同步语言 await _sessionStore.saveLocaleTag(serverLanguage); // 同步时区 await _sessionStore.saveTimezone(serverTimezone); setState(() { _locale = localeFromLanguageTag(serverLanguage); _timezone = serverTimezone; _profileSettings = profile; // ... }); } ``` --- ## Phase 5: 验证和测试 ### 5.1 单元测试 - [ ] `resolveSystemLocale` 映射正确性 - [ ] `getSystemTimezone` 返回有效IANA ID - [ ] `SessionStore` 时区存取 ### 5.2 集成测试 - [ ] 新设备首次打开 → 系统语言/时区生效 - [ ] 新用户注册 → 后端Profile正确 - [ ] 已有用户登录 → 服务器值同步 - [ ] 有本地存储 → 使用本地值 ### 5.3 手动测试 - [ ] iOS系统语言设为英文 → App显示英文 - [ ] iOS系统语言设为繁体中文 → App显示繁体 - [ ] iOS系统时区设为非上海 → App时区正确 --- ## 实施顺序 1. **Phase 1.3**: 扩展SessionStore(最小改动,无依赖) 2. **Phase 1.1-1.2**: 创建工具函数 3. **Phase 1.4**: 修改启动流程 4. **Phase 3.1**: 后端Schema扩展 5. **Phase 3.2-3.3**: 后端Service逻辑 6. **Phase 2**: 前端注册流程扩展 7. **Phase 4**: 登录后同步时区 8. **Phase 5**: 测试验证 --- ## 依赖 - 可选: `flutter_timezone` 包(如需精确时区ID) - 添加到 `apps/pubspec.yaml` - 或使用 `DateTime.now().timeZoneName` + 映射表(无依赖)