Flutter Clean 架构下的用户画像系统的设计与实现

354 阅读7分钟

第一部分:[高屋建瓴] Mobile User Profile 系统的战略重要性

在现代 App 中,“用户画像”系统不再仅仅是存储用户信息的数据库条目,它是一个动态的、可演进的、智能的资产。其战略重要性体M现于:

  1. 个性化体验的基石: 它是实现“千人千面”的根本。无论是推荐系统(商品、内容)、UI 动态调整(展示用户偏好的模块)、还是营销推送,都依赖于一个准确、实时的用户画像。

  2. 提升用户粘性和留存: 一个“懂你”的 App 能极大地提升用户满意度和使用时长。当用户感觉 App 是为他量身定做时,其迁移成本会显著增高。

  3. 数据驱动决策的引擎: 聚合的用户画像数据可以揭示用户群体的行为模式,为产品迭代、运营策略提供精准的数据支持。

  4. 未来智能化的入口: 它是端侧机器学习的“燃料”。在本地处理用户数据进行模型训练和推理,可以实现:

    • 更高响应速度: 无需网络请求,实时反馈。
    • 更强隐私保护: 敏感数据不出设备,符合越来越严格的隐私法规。
    • 离线可用性: 在无网络环境下依然可以提供部分智能服务。

因此,我们在架构设计之初就必须将其视为一个一级功能模块,而不是“个人中心”或“设置”的附属品。


第二部分:[痛点辨析] 概念边界的划分:Auth vs. Settings vs. Preferences vs. Profile

这是最容易混淆,也是导致后续架构腐化的重灾区。必须在设计之初就用“快刀斩乱麻”的方式清晰界定。

概念职责 (Responsibility)数据例子所属模块特点
认证信息 (Auth Info)“你是谁?” - 用于验证用户身份和授权访问。userId, accessToken, refreshToken, loginMethod (手机/微信)features/auth高敏感性,安全性要求最高,数据量小,相对稳定。
应用设置 (App Settings)“App 该如何为你工作?” - 用户对 App 功能行为的控制。isDarkMode, notificationEnabled, language, fontSizefeatures/settings (或 core/settings)功能性,非个人化,与用户兴趣无关,通常是布尔/枚举值。
用户偏好 (User Preferences)“你告诉我们你喜欢什么?” - 用户显式提供的主观兴趣和选择。喜欢的分类: [科幻, 悬疑],不感兴趣的标签: [体育],价格敏感度: features/user_profile (作为输入)主观性,是用户画像的原始输入数据之一,用户可直接修改。
用户画像 (User Profile/Portrait)“我们认为你是怎样的人?” - 系统通过分析显式偏好隐式行为后,得出的推断性、结构化的用户模型。inferredPersona: "价格敏感的科幻迷", engagementScore: 0.85, topCategories: ["科幻", "数码"], nextBestOffer: "XX 耳机"features/user_profile (作为核心产出)推断性、动态演进,是算法和规则的产物,通常不直接对用户暴露全部内容,是所有个性化服务的直接数据源

核心痛点解决方案:

  • 物理隔离: 在项目结构上,authuser_profile 必须是两个独立的 feature 模块。它们之间可以通过 userId 关联,但数据模型和仓库(Repository)完全分离。
  • 逻辑分离: User PreferencesUser Profile 的一部分,是其输入。UserProfile 实体则包含了 Preferences 以及更多推断出的数据。

第三部分:[架构设计] "用户画像" 模块的落地

基于我们之前的项目结构,我们新增一个 features/user_profile 模块。

flutter_ecommerce_app/
├── lib/
│   ├── features/
│   │   ├── auth/
│   │   ├── products/
│   │   ├── ...
│   │   └── **user_profile/**             # << 新增用户画像模块
│   │       ├── presentation/
│   │       │   ├── screens/onboarding_screen.dart   # 收集偏好
│   │       │   ├── screens/profile_display_screen.dart # “个人中心”的一部分
│   │       │   └── providers/user_profile_providers.dart
│   │       ├── application/
│   │       │   ├── usecases/get_user_profile_usecase.dart
│   │       │   ├── usecases/update_user_preferences_usecase.dart
│   │       │   └── usecases/track_user_behavior_usecase.dart # 用于记录隐式行为
│   │       ├── domain/
│   │       │   ├── entities/user_preferences.dart
│   │       │   ├── entities/user_profile.dart       # << 核心实体
│   │       │   ├── entities/user_behavior_event.dart
│   │       │   └── repositories/user_profile_repository.dart (interface)
│   │       └── infrastructure/
│   │           ├── datasources/profile_local_datasource.dart
│   │           ├── datasources/profile_remote_datasource.dart
│   │           ├── repositories/user_profile_repository_impl.dart
│   │           └── **services/ml_profile_generator.dart** # << 为 Core ML 预留的钩子
│   ├── core/
│   └── main.dart

Domain 层设计 (稳定内核):

// domain/entities/user_preferences.dart
class UserPreferences extends Equatable {
  final List<String> likedCategories;
  final List<String> dislikedTags;
  // ... 其他显式偏好
}

// domain/entities/user_profile.dart
class UserProfile extends Equatable {
  final UserPreferences preferences; // 包含显式偏好
  final List<String> inferredTopCategories; // 推断出的 Top 分类
  final String personaTag; // 推断出的画像标签,如 "TechEnthusiast"
  final double engagementScore; // 用户活跃度得分
  // ... 其他推断性数据
}

// domain/repositories/user_profile_repository.dart
abstract class UserProfileRepository {
  // 获取最终整合后的用户画像
  Future<UserProfile> getUserProfile();
  
  // 更新用户显式偏好
  Future<void> updateUserPreferences(UserPreferences preferences);
  
  // 记录用户隐式行为(如点击、浏览、购买)
  Future<void> logBehavior(UserBehaviorEvent event);
}

Application 层设计 (业务流程):

  • GetUserProfileUseCase: 调用 UserProfileRepository.getUserProfile(),供个人中心页面或推荐系统使用。
  • UpdateUserPreferencesUseCase: 在 Onboarding 或设置页调用,参数是 UserPreferences,内部调用 UserProfileRepository.updateUserPreferences()
  • TrackUserBehaviorUseCase: 这是关键。App 内的各种用户行为(点击商品、观看视频)都会调用这个 UseCase,它会调用 UserProfileRepository.logBehavior(),将行为数据记录下来。

第四部分:[核心难点] 为 Core ML 预留接口

这正是 Clean Architecture 发挥威力的地方。ML 模型是实现细节,属于基础设施层。

设计思路:

  1. 数据收集: UserProfileRepositoryImpl 负责从各个数据源收集原始数据。
  2. 模型调用: 将收集到的原始数据喂给一个“画像生成服务”。
  3. 结果存储: 将服务返回的 UserProfile 缓存并提供给上层。

Infrastructure 层实现:

1. 定义一个抽象的“画像生成服务”接口:

// infrastructure/services/ml_profile_generator.dart

// 这是未来 CoreML 实现需要遵循的契约
abstract class ProfileGeneratorService {
  Future<UserProfile> generateProfile({
    required UserPreferences currentPreferences,
    required List<UserBehaviorEvent> recentBehaviors,
  });
}

2. 实现 Repository,并注入这个服务:

// infrastructure/repositories/user_profile_repository_impl.dart
class UserProfileRepositoryImpl implements UserProfileRepository {
  final ProfileLocalDataSource _localDataSource;
  final ProfileGeneratorService _profileGenerator; // << 注入 ML 服务

  UserProfileRepositoryImpl(this._localDataSource, this._profileGenerator);

  @override
  Future<void> logBehavior(UserBehaviorEvent event) async {
    // 将行为事件存储到本地 (e.g., a separate Hive box or SQLite table)
    await _localDataSource.saveBehavior(event);
  }
  
  @override
  Future<void> updateUserPreferences(UserPreferences preferences) async {
    await _localDataSource.savePreferences(preferences);
  }

  @override
  Future<UserProfile> getUserProfile() async {
    // 1. 检查是否有有效的本地缓存画像
    final cachedProfile = await _localDataSource.getProfile();
    if (cachedProfile != null && !isStale(cachedProfile.timestamp)) {
      return cachedProfile;
    }

    // 2. 如果没有或已过期,则重新生成
    //    a. 从本地数据源获取原始数据
    final preferences = await _localDataSource.getPreferences();
    final behaviors = await _localDataSource.getRecentBehaviors(limit: 100);

    //    b. 调用 ML 服务进行计算和推断
    //       这里就是与 Core ML 交互的“口子”
    final newProfile = await _profileGenerator.generateProfile(
      currentPreferences: preferences,
      recentBehaviors: behaviors,
    );

    //    c. 缓存新生成的画像
    await _localDataSource.saveProfile(newProfile);

    return newProfile;
  }
}

3. 如何为 Core ML 预留口子?

  • 当前阶段: 我们可以提供一个基于规则的 RuleBasedProfileGenerator 实现,它不依赖 ML,只是简单地根据行为计数等来生成画像。
    class RuleBasedProfileGenerator implements ProfileGeneratorService {
      @override
      Future<UserProfile> generateProfile(...) {
        // 简单的逻辑:浏览次数最多的分类就是 Top 分类
        // ...
        return Future.value(UserProfile(...));
      }
    }
    
  • 未来阶段 (引入 Core ML): 你只需要创建一个新的实现 CoreMLProfileGenerator
    class CoreMLProfileGenerator implements ProfileGeneratorService {
      // 内部会通过 platform channels 调用原生的 Core ML SDK
      final MethodChannel _channel = MethodChannel('com.yourapp/coreml');
    
      @override
      Future<UserProfile> generateProfile({ ... }) async {
        // 1. 将 preferences 和 behaviors 序列化为 ML 模型需要的格式 (e.g., JSON or Map)
        final inputData = serializeForML(preferences, behaviors);
        
        // 2. 通过平台通道调用原生 Core ML 模型进行推理
        final resultJson = await _channel.invokeMethod('predictUserProfile', inputData);
        
        // 3. 将返回的 JSON 解析为 UserProfile 实体
        return UserProfile.fromJson(resultJson);
      }
    }
    

最后,在 Riverpod 的 Provider 中切换实现即可,对 App 的其他部分完全透明!

// providers.dart
final profileGeneratorProvider = Provider<ProfileGeneratorService>((ref) {
  // 在这里决定使用哪个实现
  // 可以通过环境变量、A/B Test 等方式动态切换
  if (featureFlags.isCoreMLEnabled) {
    return CoreMLProfileGenerator();
  } else {
    return RuleBasedProfileGenerator();
  }
});

final userProfileRepositoryProvider = Provider<UserProfileRepository>((ref) {
  return UserProfileRepositoryImpl(
    ref.watch(profileLocalDataSourceProvider),
    ref.watch(profileGeneratorProvider), // << 动态注入
  );
});

总结

通过上述设计,我们实现了:

  1. 概念清晰: 明确划分了认证、设置、偏好和画像的边界,避免了职责混乱。
  2. 结构合理: 将用户画像作为一个独立的、高内聚的功能模块,易于维护和扩展。
  3. 拥抱变化: 通过在 Domain 层定义稳定的 UserProfileRepository 接口,将复杂的画像生成逻辑(无论是基于规则还是 ML)封装在 Infrastructure 层。
  4. 无缝升级: 为端侧 ML 预留了清晰的、非侵入式的“挂载点” (ProfileGeneratorService)。未来引入 Core ML 时,只需添加一个新的实现类并修改一行 Provider 的构造代码,而无需触动任何业务逻辑(Use Cases)或 UI 代码,这正是顶级架构弹性的完美体现。

下一篇, 讲述这个用户画像系统如何与其他模块进行交互。