22 KiB
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已更新
- 文档已更新
- 代码可编译运行