前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

本文从学习者角度深入剖析 Flutter 猫咪健康管理应用中登录系统的完整架构设计与实现。通过 Provider 状态管理、持久化存储、路由守卫等核心技术的综合应用,构建了安全可靠的用户认证体系,并分享了在实际开发中的架构决策和优化实践。

1. 登录系统需求分析与架构设计

1.1 业务需求深度分析

在猫咪健康管理应用中,登录系统不仅是用户身份验证的入口,更是数据隔离和个性化服务的基础。我们需要支持以下核心功能:

  • 多用户数据隔离:不同用户的猫咪数据完全独立
  • 离线登录支持:在网络异常时仍可访问本地数据
  • 自动登录体验:用户无需重复输入凭证
    安全退出机制:清除敏感信息,保护用户隐私

1.2 技术架构选型依据

// 技术栈选择理由分析
class LoginArchitectureDecision {
  // 状态管理选择:Provider vs Bloc vs Riverpod
  final String stateManagement = '''
  选择Provider的原因:
  1. 学习曲线平缓,适合项目快速迭代
  2. 与Flutter生态深度集成,文档完善
  3. 满足当前登录状态的简单管理需求
  4. 团队熟悉度较高,降低开发成本
  ''';

  // 存储方案选择:SharedPreferences vs Hive vs SQLite
  final String storageSolution = '''
  选择SharedPreferences的原因:
  1. 登录数据量小,不需要复杂查询
  2. 实现简单,无需额外依赖
  3. 读写性能满足登录场景需求
  4. 系统级支持,稳定性有保障
  ''';
}
  • 架构决策类设计:通过类属性封装技术选型理由,便于团队统一理解
  • 注释文档化:将决策理由以字符串形式存储,便于查阅和维护
  • 技术对比思考:明确列出与其他方案的对比,体现架构决策的合理性

2. 核心模型设计:UserModel 的演进思考

2.1 用户模型的多版本演进

版本3.0:完整业务实体模型(当前采用)

// lib/models/user_model.dart
class UserModel {
  final String id;
  final String username;
  final String email;
  final String avatarUrl;
  final DateTime createdAt;
  List<CatModel> cats; // 业务关联:用户的猫咪列表

  // 私有构造函数 + 工厂方法的架构思考
  UserModel._({
    required this.id,
    required this.username,
    required this.email,
    required this.avatarUrl,
    required this.createdAt,
    required this.cats,
  });

  // 工厂方法设计:控制对象创建流程
  factory UserModel.create({
    required String id,
    required String username,
    required String email,
    String avatarUrl = '',
    DateTime? createdAt,
    List<CatModel> cats = const [],
  }) {
    // 输入验证层 - 防御性编程实践
    _validateInputs(id, username, email);
    
    return UserModel._(
      id: id,
      username: username.trim(),
      email: email.toLowerCase().trim(),
      avatarUrl: avatarUrl,
      createdAt: createdAt ?? DateTime.now(),
      cats: List.from(cats), // 防御性拷贝
    );
  }

  // 验证逻辑的架构意义
  static void _validateInputs(String id, String username, String email) {
    if (id.isEmpty) throw ArgumentError('用户ID不能为空');
    if (username.isEmpty) throw ArgumentError('用户名不能为空');
    if (!_isValidEmail(email)) throw ArgumentError('邮箱格式不正确');
  }

  // 邮箱验证正则表达式
  static bool _isValidEmail(String email) {
    return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email);
  }

  // 业务逻辑方法:计算用户活跃度
  int get activeCatCount => cats.where((cat) => cat.healthStatus == '健康').length;
  
  double get healthComplianceRate {
    if (cats.isEmpty) return 0.0;
    return activeCatCount / cats.length;
  }

  // JSON序列化方法 - 支持数据持久化
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'username': username,
      'email': email,
      'avatarUrl': avatarUrl,
      'createdAt': createdAt.toIso8601String(),
      'cats': cats.map((cat) => cat.toJson()).toList(),
    };
  }

  // 工厂方法:从JSON创建对象
  factory UserModel.fromJson(Map<String, dynamic> json) {
    return UserModel.create(
      id: json['id'] ?? '',
      username: json['username'] ?? '',
      email: json['email'] ?? '',
      avatarUrl: json['avatarUrl'] ?? '',
      createdAt: json['createdAt'] != null 
          ? DateTime.parse(json['createdAt'])
          : null,
    );
  }
}

代码分析讲解:

  • 私有构造函数设计:防止外部直接创建对象,强制使用工厂方法,确保对象创建的合法性
  • 工厂方法模式应用:create工厂方法封装对象创建逻辑,提供统一的创建入口
  • 防御性编程实践:_validateInputs方法验证输入参数,提前发现数据问题
  • 业务逻辑封装:activeCatCount和 healthComplianceRate计算属性封装业务规则
  • 数据持久化支持:toJson和 fromJson方法支持对象序列化,便于存储和传输
  • 默认值处理:为可选参数提供合理的默认值,增强代码健壮性

3. 认证状态管理:AuthProvider 的架构深度解析

3.1 状态机设计与生命周期管理

// lib/providers/auth_provider.dart

// 认证状态枚举 - 明确的状态定义
enum AuthStatus {
  initial,    // 初始状态
  checking,    // 检查登录状态
  authenticating, // 认证中
  authenticated,  // 已认证
  unauthenticated,// 未认证
  error,       // 错误状态
  expired,     // 会话过期
}

class AuthProvider with ChangeNotifier {
  // 状态变量设计原则:最小化暴露,最大化控制
  UserModel? _currentUser;
  AuthStatus _status = AuthStatus.initial;
  String? _errorMessage;
  DateTime? _lastActivity; // 最后活动时间,用于会话管理

  // 公开接口设计:只读访问 + 业务方法
  UserModel? get currentUser => _currentUser;
  AuthStatus get status => _status;
  String? get errorMessage => _errorMessage;
  bool get isAuthenticated => _status == AuthStatus.authenticated;
  
  // 派生状态:减少重复计算
  bool get hasCats => _currentUser?.cats.isNotEmpty ?? false;
  int get sessionDuration {
    if (_lastActivity == null) return 0;
    return DateTime.now().difference(_lastActivity).inMinutes;
  }

  // 状态转移的完整生命周期管理
  Future<void> login({
    required String username,
    required String password,
    bool rememberMe = false,
  }) async {
    // 前置条件检查:防止重复登录
    if (_status == AuthStatus.authenticating) {
      throw StateError('登录操作正在进行中');
    }

    // 状态转移:开始认证
    _updateStatus(AuthStatus.authenticating);
    _errorMessage = null;
    _lastActivity = DateTime.now();

    try {
      // 输入验证
      _validateCredentials(username, password);
      
      // 模拟网络请求
      final userData = await _performLoginRequest(username, password);
      
      // 数据验证和转换
      final user = UserModel.fromJson(userData);
      
      // 状态更新:认证成功
      _updateAuthState(
        user: user,
        newStatus: AuthStatus.authenticated,
        rememberMe: rememberMe,
      );
      
      // 持久化策略
      await _persistAuthState(user, rememberMe: rememberMe);
      
    } catch (error) {
      // 错误处理和状态回滚
      _handleAuthError(error);
      rethrow;
    }
  }

  // 输入凭证验证
  void _validateCredentials(String username, String password) {
    if (username.isEmpty) {
      throw ArgumentError('用户名不能为空');
    }
    if (password.isEmpty) {
      throw ArgumentError('密码不能为空');
    }
    if (password.length < 6) {
      throw ArgumentError('密码长度至少6位');
    }
  }

  // 模拟登录请求
  Future<Map<String, dynamic>> _performLoginRequest(
    String username, String password) async {
    // 模拟网络延迟
    await Future.delayed(Duration(seconds: 2));
    
    // 模拟登录验证逻辑
    if (username == 'admin' && password == 'password') {
      return {
        'id': '1',
        'username': username,
        'email': '$username@example.com',
        'avatarUrl': '',
        'createdAt': DateTime.now().toIso8601String(),
      };
    } else {
      throw Exception('用户名或密码错误');
    }
  }

  // 统一状态更新方法
  void _updateAuthState({
    required UserModel? user,
    required AuthStatus newStatus,
    bool rememberMe = false,
  }) {
    _currentUser = user;
    _status = newStatus;
    _errorMessage = null;
    _lastActivity = DateTime.now();
    
    // 通知监听者状态变化
    notifyListeners();
  }

  // 状态更新辅助方法
  void _updateStatus(AuthStatus newStatus) {
    _status = newStatus;
    notifyListeners();
  }
}

代码分析讲解:

  • 状态枚举设计:AuthStatus枚举明确定义所有可能的状态,避免魔法字符串
  • 封装性原则:使用私有变量 _currentUser、_status封装内部状态,通过 getter 方法提供只读访问
  • 派生状态计算:hasCats和 sessionDuration通过计算获得,避免存储冗余数据
  • 状态机模式:login方法实现完整的状态转移流程,确保状态变更的一致性
  • 异常处理策略:_validateCredentials提前验证输入,_handleAuthError统一处理错误
  • 关注点分离:将网络请求、状态更新、持久化等逻辑分离到不同方法中

3.2 错误处理与恢复机制

// 错误处理架构扩展
extension AuthErrorHandling on AuthProvider {
  // 错误分类和处理策略
  void _handleAuthError(dynamic error) {
    _updateStatus(AuthStatus.error);
    
    if (error is ArgumentError) {
      _errorMessage = '输入验证失败: ${error.message}';
    } else if (error is TimeoutException) {
      _errorMessage = '网络连接超时,请检查网络设置';
      _scheduleRetry(); // 自动重试机制
    } else if (error is SocketException) {
      _errorMessage = '网络不可用,请检查网络连接';
      _fallbackToOfflineMode(); // 离线模式降级
    } else if (error is FormatException) {
      _errorMessage = '服务器响应格式错误';
    } else {
      _errorMessage = '登录失败: ${error.toString()}';
    }
    
    notifyListeners();
  }

  // 自动重试机制
  void _scheduleRetry() {
    Future.delayed(Duration(seconds: 3), () {
      if (_status == AuthStatus.error) {
        _updateStatus(AuthStatus.unauthenticated);
        notifyListeners();
      }
    });
  }

  // 离线模式降级策略
  void _fallbackToOfflineMode() async {
    try {
      final localUser = await _tryLoadLocalUser();
      if (localUser != null) {
        _updateAuthState(
          user: localUser,
          newStatus: AuthStatus.authenticated,
        );
        print('已切换到离线模式');
      }
    } catch (e) {
      print('离线模式启动失败: $e');
    }
  }

  // 尝试加载本地用户数据
  Future<UserModel?> _tryLoadLocalUser() async {
    // 这里应该从本地存储加载用户数据
    // 简化实现,返回null表示没有本地数据
    return null;
  }
}

代码分析讲解:

  • 错误分类处理:根据异常类型提供不同的错误消息和恢复策略
  • 扩展方法应用:使用 extension将错误处理逻辑分离,保持主类简洁
  • 优雅降级策略:网络异常时尝试切换到离线模式,提升用户体验
  • 自动恢复机制:_scheduleRetry实现自动重试,减少用户手动操作
  • 异常信息友好化:将技术异常转换为用户友好的提示信息

4. 持久化存储架构:安全与性能的平衡

4.1 分层存储设计模式

// lib/services/storage_service.dart

// 抽象存储接口 - 依赖倒置原则应用
abstract class AuthStorage {
  Future<void> saveUser(UserModel user, {bool rememberMe = true});
  Future<UserModel?> loadUser();
  Future<void> clear();
  Future<bool> hasCachedCredentials();
}

// 具体实现:SharedPreferences 存储服务
class SharedPreferencesAuthStorage implements AuthStorage {
  static const String _keyUserData = 'user_data';
  static const String _keyAuthState = 'auth_state';
  static const String _keyRememberMe = 'remember_me';
  static const String _keyLastLogin = 'last_login';

  
  Future<void> saveUser(UserModel user, {bool rememberMe = true}) async {
    final prefs = await SharedPreferences.getInstance();
    
    // 原子操作:确保数据一致性
    await Future.wait([
      prefs.setString(_keyUserData, jsonEncode(user.toJson())),
      prefs.setBool(_keyAuthState, true),
      prefs.setBool(_keyRememberMe, rememberMe),
      prefs.setString(_keyLastLogin, DateTime.now().toIso8601String()),
    ]);
    
    print('用户数据保存成功: ${user.username}');
  }

  
  Future<UserModel?> loadUser() async {
    try {
      final prefs = await SharedPreferences.getInstance();
      final isAuthenticated = prefs.getBool(_keyAuthState) ?? false;
      
      if (!isAuthenticated) {
        print('没有找到认证状态');
        return null;
      }
      
      final userData = prefs.getString(_keyUserData);
      if (userData == null) {
        print('用户数据为空');
        await clear(); // 数据不一致,清理状态
        return null;
      }
      
      final userJson = jsonDecode(userData);
      return UserModel.fromJson(userJson);
      
    } catch (e) {
      print('加载用户数据失败: $e');
      await clear(); // 数据损坏时自动清理
      return null;
    }
  }

  
  Future<void> clear() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.remove(_keyUserData);
    await prefs.remove(_keyAuthState);
    await prefs.remove(_keyRememberMe);
    await prefs.remove(_keyLastLogin);
    print('用户数据已清除');
  }

  
  Future<bool> hasCachedCredentials() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getBool(_keyAuthState) ?? false;
  }
}

代码分析讲解:

  • 接口抽象设计:AuthStorage抽象接口定义存储契约,支持多种实现
  • 依赖倒置原则:高层模块依赖抽象接口,不依赖具体实现
  • 原子操作保证:使用 Future.wait确保多个存储操作的原子性
  • 错误恢复机制:数据损坏时自动清理,防止脏数据影响系统
  • 日志记录:关键操作添加日志,便于调试和问题追踪

4.2 自动登录的健壮性设计

// 自动登录的完整流程管理
extension AutoLoginManagement on AuthProvider {
  Future<void> autoLogin() async {
    // 状态检查:避免重复自动登录
    if (_status != AuthStatus.initial && _status != AuthStatus.unauthenticated) {
      print('自动登录跳过:当前状态为 $_status');
      return;
    }

    _updateStatus(AuthStatus.checking);
    print('开始自动登录检查...');
    
    try {
      final storage = SharedPreferencesAuthStorage();
      
      // 检查是否有缓存凭证
      final hasCredentials = await storage.hasCachedCredentials();
      if (!hasCredentials) {
        print('没有找到缓存凭证');
        _updateStatus(AuthStatus.unauthenticated);
        return;
      }
      
      // 加载用户数据
      final user = await storage.loadUser();
      
      if (user != null && _validateAutoLoginConditions(user)) {
        // 自动登录成功
        _updateAuthState(
          user: user,
          newStatus: AuthStatus.authenticated,
        );
        print('自动登录成功: ${user.username}');
      } else {
        // 自动登录条件不满足
        _updateStatus(AuthStatus.unauthenticated);
        await storage.clear(); // 清理无效数据
        print('自动登录条件不满足,已清理缓存');
      }
    } catch (error) {
      // 自动登录失败不影响手动登录
      print('自动登录失败: $error');
      _updateStatus(AuthStatus.unauthenticated);
    }
  }

  // 自动登录条件验证
  bool _validateAutoLoginConditions(UserModel user) {
    // 1. 基础数据验证
    if (user.id.isEmpty || user.username.isEmpty) {
      print('用户数据不完整');
      return false;
    }
    
    // 2. 会话有效期检查(示例:7天内有效)
    final prefs = await SharedPreferences.getInstance();
    final lastLoginStr = prefs.getString('last_login');
    if (lastLoginStr != null) {
      final lastLogin = DateTime.parse(lastLoginStr);
      if (DateTime.now().difference(lastLogin).inDays > 7) {
        print('会话已过期');
        return false;
      }
    }
    
    // 3. 其他业务规则验证
    return _validateBusinessRules(user);
  }

  bool _validateBusinessRules(UserModel user) {
    // 这里可以添加业务特定的验证规则
    // 例如:检查用户状态、权限等
    return true; // 简化实现
  }
}

代码分析讲解:

  • 状态机控制:自动登录前检查当前状态,避免不必要的操作
  • 条件验证链:多层验证确保自动登录的安全性
  • 优雅降级:自动登录失败时优雅回退到未认证状态
  • 资源清理:无效数据自动清理,防止状态不一致
  • 详细日志:每个步骤都有日志输出,便于问题排查

5. 路由守卫与导航架构

5.1 智能路由守卫设计

// lib/navigation/auth_guard.dart

class AuthGuard extends NavigatorObserver {
  final AuthProvider authProvider;
  final Set<String> _protectedRoutes = {
    '/home', '/profile', '/cats', '/settings'
  };

  AuthGuard(this.authProvider);

  
  void didPush(Route route, Route? previousRoute) {
    _checkRouteProtection(route);
  }

  
  void didReplace({Route? newRoute, Route? oldRoute}) {
    if (newRoute != null) {
      _checkRouteProtection(newRoute);
    }
  }

  void _checkRouteProtection(Route route) {
    final routeName = _getRouteName(route);
    print('路由守卫检查: $routeName');
    
    if (_requiresAuthentication(routeName) && !authProvider.isAuthenticated) {
      // 重定向到登录页,并记录目标路由
      _redirectToLogin(route, routeName);
    } else if (routeName == '/login' && authProvider.isAuthenticated) {
      // 已登录用户访问登录页,重定向到首页
      _redirectToHome(route);
    }
  }

  String? _getRouteName(Route route) {
    return route.settings.name;
  }

  bool _requiresAuthentication(String? routeName) {
    return routeName != null && _protectedRoutes.contains(routeName);
  }

  void _redirectToLogin(Route route, String? targetRoute) {
    print('重定向到登录页,目标路由: $targetRoute');
    
    WidgetsBinding.instance.addPostFrameCallback((_) {
      final context = route.navigator!.context;
      Navigator.of(context).pushReplacementNamed(
        '/login',
        arguments: {'returnUrl': targetRoute},
      );
    });
  }

  void _redirectToHome(Route route) {
    print('已登录用户重定向到首页');
    
    WidgetsBinding.instance.addPostFrameCallback((_) {
      final context = route.navigator!.context;
      Navigator.of(context).pushReplacementNamed('/home');
    });
  }
}

代码分析讲解:

  • 导航观察者模式:继承 NavigatorObserver监听路由变化
  • 路由保护策略:_protectedRoutes定义需要认证的路由集合
  • 双重检查逻辑:未认证用户访问保护路由时重定向到登录页,已认证用户访问登录页时重定向到首页
  • 异步重定向:使用 addPostFrameCallback确保在界面构建完成后执行重定向
  • 目标路由传递:通过 arguments传递原始目标路由,支持登录后跳转

5.2 依赖注入与应用初始化

// lib/main.dart - 应用启动架构

void main() async {
  // 确保Flutter绑定初始化
  WidgetsFlutterBinding.ensureInitialized();
  
  // 错误处理配置
  _configureErrorHandling();
  
  // 依赖注入容器初始化
  final authProvider = await _initializeDependencies();
  
  runApp(MyApp(authProvider: authProvider));
}

Future<AuthProvider> _initializeDependencies() async {
  print('初始化依赖注入容器...');
  
  // 创建AuthProvider实例
  final authProvider = AuthProvider();
  
  // 执行自动登录检查
  print('执行自动登录检查...');
  await authProvider.autoLogin();
  
  return authProvider;
}

void _configureErrorHandling() {
  // 全局错误处理
  FlutterError.onError = (details) {
    print('Flutter错误: ${details.exception}');
    if (kDebugMode) {
      FlutterError.dumpErrorToConsole(details);
    }
  };
  
  // 异步错误处理
  PlatformDispatcher.instance.onError = (error, stack) {
    print('全局错误: $error\n$stack');
    return true; // 不阻止错误传播
  };
}

class MyApp extends StatelessWidget {
  final AuthProvider authProvider;
  
  const MyApp({super.key, required this.authProvider});

  
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => authProvider),
        // 其他Provider可以在这里添加
      ],
      child: MaterialApp(
        title: '猫咪健康助手',
        theme: ThemeData(
          primarySwatch: Colors.orange,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        navigatorObservers: [
          AuthGuard(authProvider), // 注册路由守卫
        ],
        home: Consumer<AuthProvider>(
          builder: (context, auth, child) {
            // 根据认证状态显示不同页面
            return auth.isAuthenticated ? HomePage() : LoginPage();
          },
        ),
        routes: {
          '/login': (context) => LoginPage(),
          '/home': (context) => HomePage(),
          // 其他路由...
        },
      ),
    );
  }
}

代码分析讲解:

  • 应用初始化流程:main函数中完成所有初始化工作
  • 错误处理配置:配置全局错误处理,提高应用稳定性
  • 依赖注入容器:使用 MultiProvider管理所有状态提供者
  • 路由守卫注册:将 AuthGuard注册到导航观察者中
  • 条件路由:根据认证状态动态决定首页内容
    主题配置:统一应用主题,保持UI一致性

6. 登录页面UI/UX架构设计

6.1 响应式登录表单架构

// lib/pages/login/login_page.dart

class LoginPage extends StatefulWidget {
  final String? returnUrl;
  
  const LoginPage({super.key, this.returnUrl});

  
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final _formKey = GlobalKey<FormState>();
  final _usernameController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _rememberMe = true;
  bool _obscurePassword = true;
  bool _isLoading = false;

  
  void initState() {
    super.initState();
    print('登录页面初始化,返回URL: ${widget.returnUrl}');
  }

  
  void dispose() {
    _usernameController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey[50],
      body: SafeArea(
        child: Consumer<AuthProvider>(
          builder: (context, authProvider, child) {
            return _buildResponsiveLayout(authProvider, context);
          },
        ),
      ),
    );
  }

  Widget _buildResponsiveLayout(AuthProvider authProvider, BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        final isLargeScreen = constraints.maxWidth > 600;
        
        return isLargeScreen 
            ? _buildDesktopLayout(authProvider, context)
            : _buildMobileLayout(authProvider, context);
      },
    );
  }

  Widget _buildMobileLayout(AuthProvider authProvider, BuildContext context) {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(24),
      child: Column(
        children: [
          const SizedBox(height: 40),
          _buildAppLogo(),
          const SizedBox(height: 40),
          _buildLoginForm(authProvider, context),
          const SizedBox(height: 20),
          _buildAdditionalOptions(),
        ],
      ),
    );
  }

  Widget _buildAppLogo() {
    return Column(
      children: [
        Icon(
          Icons.pets,
          size: 80,
          color: Colors.orange,
        ),
        const SizedBox(height: 16),
        Text(
          '猫咪健康助手',
          style: TextStyle(
            fontSize: 24,
            fontWeight: FontWeight.bold,
            color: Colors.grey[800],
          ),
        ),
        const SizedBox(height: 8),
        Text(
          '管理您的猫咪健康',
          style: TextStyle(
            fontSize: 14,
            color: Colors.grey[600],
          ),
        ),
      ],
    );
  }
}

7.效果展示

7.1展示

  • 点击设置页面,会收到登录请求
    在这里插入图片描述
  • 新用户填写注册信息过后自动登录
    在这里插入图片描述

在这里插入图片描述

  • 已有账号可以直接登录,密码为6位数字,输入不正确会提示
    在这里插入图片描述
  • 通过点击修改个人信息可以修改用户名和邮箱,修改后自动保存
    在这里插入图片描述
  • 提供修改密码服务
    在这里插入图片描述
  • 退出登录会收到提醒
    在这里插入图片描述

总结

通过本次登录系统架构实践,深入掌握了Provider状态管理、持久化存储与路由守卫的核心技术应用,构建了安全可靠的用户认证体系,为Flutter应用开发奠定了坚实架构基础。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐