Files
kitchen/docs/superpowers/plans/2026-04-12-api-migration.md
Developer 13fdbdc431 瀑布流
2026-04-13 03:39:29 +08:00

22 KiB
Raw Blame History

API v3.2.0 迁移实施计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 将App代码适配API v3.2.0,移除已删除接口,更新评分系统

Architecture:

  • 移除 api_preference.php 依赖,改为本地 SharedPreferences 存储
  • recommend 接口迁移为 rate 评分接口
  • 移除 personal 信息流接口
  • 更新热门排行排序参数

Tech Stack: Flutter, Dio, SharedPreferences, GetX


API变化摘要

已删除接口

接口 说明 替代方案
api_preference.php 用户偏好设置 本地 SharedPreferences 存储
api_feed.php?act=personal 个性化信息流 使用 recommend 替代
api.php?act=list&use_preference=true 偏好筛选 移除参数

接口变更

原接口 新接口 变更说明
api_action.php?act=recommend api_action.php?act=rate 推荐改为评分(1-5分)
recommend_nums/recommend_score rate_nums/rate_score 字段重命名
api_hot.php?sort=recommend api_hot.php?sort=rate 排序参数变更

文件修改清单

文件 操作 说明
lib/src/config/api_config.dart 修改 移除 preference 接口定义
lib/src/repositories/preference_repository.dart 重构 改为本地存储实现
lib/src/repositories/action_repository.dart 修改 recommend → rate
lib/src/repositories/feed_repository.dart 修改 移除 personal 接口
lib/src/repositories/hot_repository.dart 修改 sort 参数更新
lib/src/controllers/user/preference_controller.dart 修改 适配本地存储
lib/src/models/user_preference_model.dart 检查 确保模型兼容
docs/dev/UNFINISHED_FEATURES.md 更新 记录API变更

Task 1: 更新 api_config.dart

Files:

  • Modify: lib/src/config/api_config.dart

  • Step 1: 移除 preference 接口定义

// 2026-04-09 | ApiConfig | API路由配置 | 新建对齐eat.wktyl.com后端接口
// 2026-04-10 | API v2.0.0 迁移:移除 api_unified/api_hot/api_online/api_request_stats合并到 api.php 和 stats_full.php
// 2026-04-12 | API v3.2.0 迁移:移除 api_preference.php已删除偏好设置改为本地存储
class ApiConfig {
  static const String baseUrl = 'http://eat.wktyl.com/api';

  // 主接口 — 列表、详情、搜索、统计、统一输出
  static const String recipe = '/api.php';

  // 动态接口 — 点赞、评分、浏览量recommend已改为rate
  static const String action = '/api_action.php';

  // 智能选择 — 随机推荐、动态筛选
  static const String whatToEat = '/api_what_to_eat.php';

  // 信息流 — 推荐、热门、最新personal已删除
  static const String feed = '/api_feed.php';

  // 全面统计 — 热门、在线、请求统计
  static const String statsFull = '/stats_full.php';

  // 筛选接口 — 分类筛选、标签筛选、时段筛选
  static const String filter = '/api_filter.php';

  // 发现页 — 随机数据、瀑布流
  static const String discover = '/api_discover.php';

  // 运维工具
  static const String cacheManage = '/cache_manage.php';
  static const String diagnose = '/diagnose.php';

  // 静态数据资源
  static const String assetsBase = 'http://eat.wktyl.com/api/assets';
  static const String eatingTimesJson = '$assetsBase/eating_times.json';
  static const String nutritionTypesJson = '$assetsBase/nutrition_types.json';
  static const String allergenDataJson = '$assetsBase/gmy.json';

  // 缓存控制参数
  static const String paramRefresh = '_refresh';
  static const String paramStale = '_stale';
  static const String paramFormat = '_format';
  static const String paramPretty = '_pretty';
  static const String paramDebug = '_debug';

  // 响应格式
  static const String formatJson = 'json';
  static const String formatGzip = 'gzip';
  static const String formatMsgpack = 'msgpack';

  static Map<String, dynamic> refreshParams({Map<String, dynamic>? extra}) {
    return {paramRefresh: '1', ...?extra};
  }

  static Map<String, dynamic> staleParams({Map<String, dynamic>? extra}) {
    return {paramStale: '1', ...?extra};
  }

  static Map<String, dynamic> debugParams({Map<String, dynamic>? extra}) {
    return {paramDebug: '1', ...?extra};
  }

  static Map<String, dynamic> gzipParams({Map<String, dynamic>? extra}) {
    return {paramFormat: formatGzip, ...?extra};
  }
}

Task 2: 重构 preference_repository.dart

Files:

  • Modify: lib/src/repositories/preference_repository.dart

  • Step 1: 重写为本地存储实现

// 2026-04-10 | PreferenceRepository | 用户偏好仓库 | API v2.0.0: act=set→save, allergens 增加 user_id 参数
// 2026-04-12 | API v3.2.0: api_preference.php已删除改为本地SharedPreferences存储
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:mom_kitchen/src/models/user_preference_model.dart';

class PreferenceRepository {
  static const String _keyPreferredTags = 'user_preferred_tags';
  static const String _keyPreferredCategories = 'user_preferred_categories';
  static const String _keyBlockedAllergens = 'user_blocked_allergens';
  static const String _keyUserId = 'user_id';

  SharedPreferences? _prefs;

  Future<SharedPreferences> _getPrefs() async {
    _prefs ??= await SharedPreferences.getInstance();
    return _prefs!;
  }

  // ─── 获取偏好 ───

  Future<UserPreferenceModel> getPreference({required String userId}) async {
    try {
      final prefs = await _getPrefs();
      
      final preferredTags = prefs.getStringList(_keyPreferredTags) ?? [];
      final preferredCategories = prefs.getStringList(_keyPreferredCategories) ?? [];
      final blockedAllergens = prefs.getStringList(_keyBlockedAllergens) ?? [];

      return UserPreferenceModel(
        userId: userId,
        preferredTags: preferredTags.map(int.parse).toList(),
        preferredCategories: preferredCategories.map(int.parse).toList(),
        blockedAllergens: blockedAllergens,
      );
    } catch (e, stackTrace) {
      debugPrint('PreferenceRepository.getPreference error: $e');
      debugPrint('Stack trace: $stackTrace');
      return UserPreferenceModel(userId: userId);
    }
  }

  Future<String?> getStoredUserId() async {
    try {
      final prefs = await _getPrefs();
      return prefs.getString(_keyUserId);
    } catch (_) {
      return null;
    }
  }

  Future<void> storeUserId(String userId) async {
    try {
      final prefs = await _getPrefs();
      await prefs.setString(_keyUserId, userId);
      debugPrint('PreferenceRepository: Stored user ID: $userId');
    } catch (e) {
      debugPrint('PreferenceRepository.storeUserId error: $e');
    }
  }

  // ─── 保存偏好 ───

  Future<UserPreferenceModel> savePreference({
    required String userId,
    List<int>? preferredTags,
    List<int>? preferredCategories,
    List<String>? blockedAllergens,
  }) async {
    try {
      final prefs = await _getPrefs();

      if (preferredTags != null) {
        await prefs.setStringList(
          _keyPreferredTags,
          preferredTags.map((e) => e.toString()).toList(),
        );
      }

      if (preferredCategories != null) {
        await prefs.setStringList(
          _keyPreferredCategories,
          preferredCategories.map((e) => e.toString()).toList(),
        );
      }

      if (blockedAllergens != null) {
        await prefs.setStringList(_keyBlockedAllergens, blockedAllergens);
      }

      await storeUserId(userId);

      return UserPreferenceModel(
        userId: userId,
        preferredTags: preferredTags ?? [],
        preferredCategories: preferredCategories ?? [],
        blockedAllergens: blockedAllergens ?? [],
      );
    } catch (e, stackTrace) {
      debugPrint('PreferenceRepository.savePreference error: $e');
      debugPrint('Stack trace: $stackTrace');
      rethrow;
    }
  }

  // ─── 兼容旧 setPreference 方法 ───

  Future<UserPreferenceModel> setPreference({
    required String userId,
    List<int>? preferredTags,
    List<int>? preferredCategories,
    List<String>? blockedAllergens,
  }) {
    return savePreference(
      userId: userId,
      preferredTags: preferredTags,
      preferredCategories: preferredCategories,
      blockedAllergens: blockedAllergens,
    );
  }

  // ─── 标签操作 ───

  Future<Map<String, dynamic>> addTag({
    required String userId,
    required int tagId,
  }) async {
    try {
      final prefs = await _getPrefs();
      final tags = prefs.getStringList(_keyPreferredTags) ?? [];
      final tagStr = tagId.toString();
      if (!tags.contains(tagStr)) {
        tags.add(tagStr);
        await prefs.setStringList(_keyPreferredTags, tags);
      }
      return {'success': true, 'tag_id': tagId};
    } catch (e, stackTrace) {
      debugPrint('PreferenceRepository.addTag error: $e');
      debugPrint('Stack trace: $stackTrace');
      rethrow;
    }
  }

  Future<Map<String, dynamic>> removeTag({
    required String userId,
    required int tagId,
  }) async {
    try {
      final prefs = await _getPrefs();
      final tags = prefs.getStringList(_keyPreferredTags) ?? [];
      final tagStr = tagId.toString();
      tags.remove(tagStr);
      await prefs.setStringList(_keyPreferredTags, tags);
      return {'success': true, 'tag_id': tagId};
    } catch (e, stackTrace) {
      debugPrint('PreferenceRepository.removeTag error: $e');
      debugPrint('Stack trace: $stackTrace');
      rethrow;
    }
  }

  // ─── 分类操作 ───

  Future<Map<String, dynamic>> addCategory({
    required String userId,
    required int categoryId,
  }) async {
    try {
      final prefs = await _getPrefs();
      final categories = prefs.getStringList(_keyPreferredCategories) ?? [];
      final catStr = categoryId.toString();
      if (!categories.contains(catStr)) {
        categories.add(catStr);
        await prefs.setStringList(_keyPreferredCategories, categories);
      }
      return {'success': true, 'category_id': categoryId};
    } catch (e, stackTrace) {
      debugPrint('PreferenceRepository.addCategory error: $e');
      debugPrint('Stack trace: $stackTrace');
      rethrow;
    }
  }

  Future<Map<String, dynamic>> removeCategory({
    required String userId,
    required int categoryId,
  }) async {
    try {
      final prefs = await _getPrefs();
      final categories = prefs.getStringList(_keyPreferredCategories) ?? [];
      final catStr = categoryId.toString();
      categories.remove(catStr);
      await prefs.setStringList(_keyPreferredCategories, categories);
      return {'success': true, 'category_id': categoryId};
    } catch (e, stackTrace) {
      debugPrint('PreferenceRepository.removeCategory error: $e');
      debugPrint('Stack trace: $stackTrace');
      rethrow;
    }
  }

  // ─── 过敏原操作 ───

  Future<Map<String, dynamic>> addAllergen({
    required String userId,
    required String allergenType,
  }) async {
    try {
      final prefs = await _getPrefs();
      final allergens = prefs.getStringList(_keyBlockedAllergens) ?? [];
      if (!allergens.contains(allergenType)) {
        allergens.add(allergenType);
        await prefs.setStringList(_keyBlockedAllergens, allergens);
      }
      return {'success': true, 'allergen_type': allergenType};
    } catch (e, stackTrace) {
      debugPrint('PreferenceRepository.addAllergen error: $e');
      debugPrint('Stack trace: $stackTrace');
      rethrow;
    }
  }

  Future<Map<String, dynamic>> removeAllergen({
    required String userId,
    required String allergenType,
  }) async {
    try {
      final prefs = await _getPrefs();
      final allergens = prefs.getStringList(_keyBlockedAllergens) ?? [];
      allergens.remove(allergenType);
      await prefs.setStringList(_keyBlockedAllergens, allergens);
      return {'success': true, 'allergen_type': allergenType};
    } catch (e, stackTrace) {
      debugPrint('PreferenceRepository.removeAllergen error: $e');
      debugPrint('Stack trace: $stackTrace');
      rethrow;
    }
  }

  // ─── 过敏原列表(本地默认值) ───

  Future<List<AllergenItem>> fetchAllergens({String? userId}) async {
    return AllergenItem.defaults;
  }

  // ─── 分类列表(从 api.php 获取) ───

  Future<List<PreferenceCategory>?> fetchCategories() async {
    return null;
  }

  // ─── 标签列表(从 api.php 获取) ───

  Future<List<PreferenceTag>?> fetchTags() async {
    return null;
  }

  // ─── 清除偏好 ───

  Future<Map<String, dynamic>> clearPreference({required String userId}) async {
    try {
      final prefs = await _getPrefs();
      await prefs.remove(_keyPreferredTags);
      await prefs.remove(_keyPreferredCategories);
      await prefs.remove(_keyBlockedAllergens);
      return {'success': true};
    } catch (e, stackTrace) {
      debugPrint('PreferenceRepository.clearPreference error: $e');
      debugPrint('Stack trace: $stackTrace');
      rethrow;
    }
  }
}

Task 3: 更新 action_repository.dart

Files:

  • Modify: lib/src/repositories/action_repository.dart

  • Step 1: 将 recommend 改为 rate 评分接口

// 2026-04-09 | ActionRepository | 互动操作仓库 | 封装api_action.php调用
// 2026-04-09 | 修改写操作使用POST方法符合REST规范
// 2026-04-09 | 添加429限流错误友好提示
// 2026-04-12 | API v3.2.0: recommend接口改为rate评分接口(1-5分)
import 'package:mom_kitchen/src/config/api_config.dart';
import 'package:mom_kitchen/src/models/api_response.dart';
import 'package:mom_kitchen/src/services/api/api_service.dart';
import 'package:mom_kitchen/src/services/api/api_exception.dart';
import 'package:mom_kitchen/src/services/ui/toast_service.dart';

class ActionRepository {
  final ApiService _api = ApiService();

  Future<Map<String, dynamic>> like({
    required String type,
    required int id,
    required String action,
  }) async {
    final response = await _api.post(
      ApiConfig.action,
      data: {'act': 'like', 'type': type, 'id': id, 'action': action},
    );
    final apiResponse = ApiResponse.fromJson(
      response.data as Map<String, dynamic>,
      null,
    );
    if (!apiResponse.isSuccess) throw Exception(apiResponse.message);
    return apiResponse.data as Map<String, dynamic>? ?? {};
  }

  // ─── 评分接口原recommend改为rate ───

  Future<Map<String, dynamic>> rate({
    required String type,
    required int id,
    required int score,
  }) async {
    if (score < 1 || score > 5) {
      throw ArgumentError('评分必须在1-5之间');
    }

    try {
      final response = await _api.post(
        ApiConfig.action,
        data: {
          'act': 'rate',
          'type': type,
          'id': id,
          'score': score,
        },
      );
      final apiResponse = ApiResponse.fromJson(
        response.data as Map<String, dynamic>,
        null,
      );
      if (!apiResponse.isSuccess) throw Exception(apiResponse.message);
      return apiResponse.data as Map<String, dynamic>? ?? {};
    } on ApiException catch (e) {
      if (e.isRateLimited) {
        await ToastService.warning(
          '评分次数已达今日上限,请明天再试',
          duration: const Duration(seconds: 3),
        );
        rethrow;
      }
      rethrow;
    }
  }

  // ─── 兼容旧 recommend 方法(已废弃,请使用 rate ───
  @Deprecated('Use rate() instead. recommend接口已改为rate评分接口')
  Future<Map<String, dynamic>> recommend({
    required String type,
    required int id,
    required String action,
    int? score,
  }) async {
    return rate(type: type, id: id, score: score ?? 5);
  }

  Future<Map<String, dynamic>> view({
    required String type,
    required int id,
    int count = 1,
  }) async {
    final response = await _api.post(
      ApiConfig.action,
      data: {'act': 'view', 'type': type, 'id': id, 'count': count},
    );
    final apiResponse = ApiResponse.fromJson(
      response.data as Map<String, dynamic>,
      null,
    );
    if (!apiResponse.isSuccess) throw Exception(apiResponse.message);
    return apiResponse.data as Map<String, dynamic>? ?? {};
  }

  Future<IpStatus> fetchIpStatus() async {
    final response = await _api.get(
      ApiConfig.action,
      queryParameters: {'act': 'ip_status'},
    );
    final apiResponse = ApiResponse.fromJson(
      response.data as Map<String, dynamic>,
      null,
    );
    if (!apiResponse.isSuccess) throw Exception(apiResponse.message);
    return IpStatus.fromJson(apiResponse.data as Map<String, dynamic>? ?? {});
  }
}

class IpStatus {
  final String ip;
  final int rateUsed;
  final int rateLimit;
  final int rateRemaining;

  const IpStatus({
    required this.ip,
    required this.rateUsed,
    required this.rateLimit,
    required this.rateRemaining,
  });

  bool get canRate => rateRemaining > 0;

  // 兼容旧字段名
  @Deprecated('Use canRate instead')
  bool get canRecommend => canRate;

  factory IpStatus.fromJson(Map<String, dynamic> json) {
    return IpStatus(
      ip: json['ip'] as String? ?? '',
      rateUsed: json['today_rate_count'] as int? ?? 
                json['rate_used'] as int? ?? 0,
      rateLimit: json['daily_limit'] as int? ?? 
                 json['rate_limit'] as int? ?? 30,
      rateRemaining: json['remaining_rate'] as int? ?? 
                     json['rate_remaining'] as int? ?? 30,
    );
  }
}

Task 4: 更新 feed_repository.dart

Files:

  • Modify: lib/src/repositories/feed_repository.dart

  • Step 1: 移除 personal 接口

// 2026-04-09 | FeedRepository | 信息流数据仓库 | 封装api_feed.php调用
// 2026-04-09 | 新增excludeIds参数支持避免重复内容
// 2026-04-12 | API v3.2.0: 移除personal接口已删除
import 'package:mom_kitchen/src/config/api_config.dart';
import 'package:mom_kitchen/src/models/api_response.dart';
import 'package:mom_kitchen/src/models/feed_item_model.dart';
import 'package:mom_kitchen/src/services/api/api_service.dart';

enum FeedType { recommend, latest, hot }

class FeedRepository {
  final ApiService _api = ApiService();

  Future<PaginatedData<FeedItemModel>> fetchFeed(
    FeedType type, {
    int page = 1,
    int limit = 20,
    bool refresh = false,
    bool debug = false,
    List<int> excludeIds = const [],
  }) async {
    final params = <String, dynamic>{
      'act': type.name,
      'page': page,
      'limit': limit,
    };
    if (refresh) params[ApiConfig.paramRefresh] = '1';
    if (debug) params[ApiConfig.paramDebug] = '1';
    if (excludeIds.isNotEmpty) {
      params['exclude'] = excludeIds.join(',');
    }

    final response = await _api.get(ApiConfig.feed, queryParameters: params);
    final apiResponse = ApiResponse.fromJson(
      response.data as Map<String, dynamic>,
      (data) => PaginatedData.fromJson(
        data as Map<String, dynamic>,
        (e) => FeedItemModel.fromJson(e),
      ),
    );
    if (!apiResponse.isSuccess || apiResponse.data == null) {
      throw Exception(apiResponse.message);
    }
    return apiResponse.data!;
  }

  Future<PaginatedData<FeedItemModel>> fetchRecommend({
    int page = 1,
    int limit = 20,
    bool refresh = false,
    List<int> excludeIds = const [],
  }) => fetchFeed(
    FeedType.recommend,
    page: page,
    limit: limit,
    refresh: refresh,
    excludeIds: excludeIds,
  );

  Future<PaginatedData<FeedItemModel>> fetchLatest({
    int page = 1,
    int limit = 20,
    bool refresh = false,
    List<int> excludeIds = const [],
  }) => fetchFeed(
    FeedType.latest,
    page: page,
    limit: limit,
    refresh: refresh,
    excludeIds: excludeIds,
  );

  Future<PaginatedData<FeedItemModel>> fetchHot({
    int page = 1,
    int limit = 20,
    bool refresh = false,
    List<int> excludeIds = const [],
  }) => fetchFeed(
    FeedType.hot,
    page: page,
    limit: limit,
    refresh: refresh,
    excludeIds: excludeIds,
  );

  Future<List<FeedItemModel>> prefetch({int pages = 3}) async {
    final params = <String, dynamic>{'act': 'prefetch', 'pages': pages};

    final response = await _api.get(ApiConfig.feed, queryParameters: params);
    final apiResponse = ApiResponse.fromJson(
      response.data as Map<String, dynamic>,
      null,
    );
    if (!apiResponse.isSuccess || apiResponse.data == null) {
      throw Exception(apiResponse.message);
    }

    final data = apiResponse.data;
    if (data is List) {
      return data
          .map((e) => FeedItemModel.fromJson(e as Map<String, dynamic>))
          .toList();
    }
    return [];
  }
}

Task 5: 更新 hot_repository.dart

Files:

  • Modify: lib/src/repositories/hot_repository.dart

  • Step 1: 更新 sort 参数

主要变更:sort=recommend 改为 sort=rate,但当前代码使用的是 stats_full.php,需要确认是否需要修改。

当前代码使用 stats_full.php?act=hot,不需要修改 sort 参数,因为后端已经处理。


Task 6: 检查并更新 Controller 文件

Files:

  • Check: lib/src/controllers/user/preference_controller.dart
  • Check: lib/src/controllers/feed/feed_controller.dart
  • Check: lib/src/controllers/recipe_detail_controller.dart

需要检查这些Controller是否使用了已删除的接口。


Task 7: 更新文档

Files:

  • Modify: docs/dev/UNFINISHED_FEATURES.md

  • Step 1: 添加API迁移记录

在文档中添加API v3.2.0迁移说明,记录已删除接口和替代方案。


验收标准

  • 所有已删除接口的调用已移除或替换
  • preference_repository.dart 使用本地存储
  • action_repository.dart 使用 rate 评分接口
  • feed_repository.dart 移除 personal 接口
  • 相关Controller已更新
  • 文档已更新
  • 代码可编译运行