feat: 实现站内通知系统
- 后端: 新增 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 及错误码注册
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
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',
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user