3f3d613d99
- 后端: 新增 notifications/user_notifications 表迁移及 ORM 模型
- 后端: 实现 schema/repository/service/router 全套通知 API
- GET /api/v1/notifications (列表+游标分页)
- GET /api/v1/notifications/unread-count
- PATCH /api/v1/notifications/{id}/read (幂等)
- PATCH /api/v1/notifications/mark-all-read (幂等)
- 后端: payload 使用 Pydantic discriminated union (none/open_route/open_url)
- 后端: 19 个单元测试全部通过
- Flutter: 通知 feature 完整实现 (models/apis/repositories/bloc/UI)
- Flutter: Home 页通知按钮接入真实页面,显示未读 badge
- Flutter: 14 个测试全部通过
- 协议文档: notification-inbox-protocol.md 及错误码注册
114 lines
3.2 KiB
Dart
114 lines
3.2 KiB
Dart
import 'package:dio/dio.dart';
|
|
|
|
import '../../../../core/logging/logger.dart';
|
|
import '../../../../core/network/api_problem.dart';
|
|
import '../../../../data/network/api_client.dart';
|
|
import '../models/notification_item.dart';
|
|
import '../models/notification_list_result.dart';
|
|
|
|
class NotificationApi {
|
|
NotificationApi({required ApiClient apiClient}) : _apiClient = apiClient;
|
|
|
|
final ApiClient _apiClient;
|
|
final Logger _logger = getLogger('features.notifications.data.apis');
|
|
|
|
Future<NotificationListResult> listNotifications({
|
|
int limit = 20,
|
|
String? cursor,
|
|
}) async {
|
|
final queryParts = <String>['limit=$limit'];
|
|
if (cursor != null) {
|
|
queryParts.add('cursor=$cursor');
|
|
}
|
|
final path = '/api/v1/notifications?${queryParts.join("&")}';
|
|
|
|
try {
|
|
final json = await _apiClient.getJson(path);
|
|
final itemsJson = json['items'] as List<dynamic>? ?? [];
|
|
final items = itemsJson
|
|
.map((e) => parseNotificationItem(e as Map<String, dynamic>))
|
|
.toList();
|
|
return NotificationListResult(
|
|
items: items,
|
|
nextCursor: json['nextCursor'] as String?,
|
|
hasMore: json['hasMore'] as bool? ?? false,
|
|
);
|
|
} on DioException catch (error, stackTrace) {
|
|
_logger.error(
|
|
message: 'List notifications failed',
|
|
error: error,
|
|
stackTrace: stackTrace,
|
|
);
|
|
throw _mapProblem(error);
|
|
}
|
|
}
|
|
|
|
Future<int> getUnreadCount() async {
|
|
try {
|
|
final json = await _apiClient.getJson(
|
|
'/api/v1/notifications/unread-count',
|
|
);
|
|
return json['count'] as int? ?? 0;
|
|
} on DioException catch (error, stackTrace) {
|
|
_logger.error(
|
|
message: 'Get unread count failed',
|
|
error: error,
|
|
stackTrace: stackTrace,
|
|
);
|
|
throw _mapProblem(error);
|
|
}
|
|
}
|
|
|
|
Future<NotificationItem> markRead({required String notificationId}) async {
|
|
try {
|
|
final response = await _apiClient.rawDio.patch<Map<String, dynamic>>(
|
|
'/api/v1/notifications/$notificationId/read',
|
|
);
|
|
return parseNotificationItem(response.data!);
|
|
} on DioException catch (error, stackTrace) {
|
|
_logger.error(
|
|
message: 'Mark read failed',
|
|
error: error,
|
|
stackTrace: stackTrace,
|
|
);
|
|
throw _mapProblem(error);
|
|
}
|
|
}
|
|
|
|
Future<int> markAllRead() async {
|
|
try {
|
|
final response = await _apiClient.rawDio.patch<Map<String, dynamic>>(
|
|
'/api/v1/notifications/mark-all-read',
|
|
);
|
|
return response.data?['updatedCount'] as int? ?? 0;
|
|
} on DioException catch (error, stackTrace) {
|
|
_logger.error(
|
|
message: 'Mark all read failed',
|
|
error: error,
|
|
stackTrace: stackTrace,
|
|
);
|
|
throw _mapProblem(error);
|
|
}
|
|
}
|
|
|
|
ApiProblem _mapProblem(DioException error) {
|
|
final status = error.response?.statusCode ?? 500;
|
|
final data = error.response?.data;
|
|
|
|
if (data is Map<String, dynamic>) {
|
|
return ApiProblem(
|
|
status: status,
|
|
title: (data['title'] as String?) ?? 'Request failed',
|
|
detail: (data['detail'] as String?) ?? '',
|
|
code: data['code'] as String?,
|
|
);
|
|
}
|
|
|
|
return ApiProblem(
|
|
status: status,
|
|
title: 'Network error',
|
|
detail: error.message ?? 'Request failed',
|
|
);
|
|
}
|
|
}
|