import 'package:patbond_flutter/core/network/api_client.dart'; import 'package:patbond_flutter/core/network/api_exception.dart'; import 'package:patbond_flutter/features/community/community_exceptions.dart'; import 'package:patbond_flutter/features/community/community_models.dart'; import 'package:uuid/uuid.dart'; /// community / media 域仓库接口(契约 v1.3.0 的 13 路径 / 19 操作全覆盖; /// 页面依赖此抽象,widget / controller 测试注入假实现)。 /// /// media 两步上传本单只到协议层(创建上传 / 确认上传);预签名 PUT /// 直传对象存储不走业务信封与 Bearer 鉴权,属 T3-13 独立客户端。 abstract class CommunityRepository { // ---- media 两步上传(协议层)---- Future createMediaUpload( CreateMediaUploadRequest request, ); Future completeMediaUpload(String assetId); // ---- 帖子 CRUD / 发布 ---- Future createPost(CreatePostRequest request); Future getPost(String postId); Future updatePost(String postId, UpdatePostRequest request); Future deletePost(String postId); Future> listMyPosts({ int? limit, String? cursor, PostStatus? status, }); // ---- Feed(cursor 分页)---- Future> getFeed({int? limit, String? cursor}); // ---- 评论 ---- Future> listComments( String postId, { int? limit, String? cursor, }); Future createComment( String postId, CreateCommentRequest request, ); Future deleteComment(String commentId); // ---- 点赞 / 收藏(PUT/DELETE 语义幂等,响应权威终态)---- Future likePost(String postId); Future unlikePost(String postId); Future bookmarkPost(String postId); Future unbookmarkPost(String postId); Future> listMyBookmarks({int? limit, String? cursor}); // ---- 关注 ---- Future followUser(String userId); Future unfollowUser(String userId); Future getFollowStats(String userId); } /// 基于 [ApiClient] 的实现。全部端点走 Bearer 鉴权(复用既有 token /// 拦截 + 401/40101 单飞刷新重放);community 域业务错误码升格为类型化异常。 /// /// 幂等:createPost / createComment 两个 POST 按契约**必带** /// `Idempotency-Key`(1~128 字符;每次逻辑提交换新键;token 刷新后的 /// 自动重放沿用同一个键——键在本层每次调用生成一次,重放走同一 headers)。 /// 点赞/收藏/关注为 PUT/DELETE 语义幂等,无需幂等键。 class ApiCommunityRepository implements CommunityRepository { ApiCommunityRepository({required this._api, this._uuid = const Uuid()}); final ApiClient _api; final Uuid _uuid; Future _request( String path, { String method = 'GET', Object? body, Map? query, bool idempotent = false, }) async { try { return await _api.request( path, method: method, body: body, query: query, headers: idempotent ? {'Idempotency-Key': _uuid.v4()} : null, requiresAuth: true, ); } on ApiBusinessException catch (error) { throw mapCommunityBusinessException(error); } } Map _asMap(Object? data) => data! as Map; // ---- media ---- @override Future createMediaUpload( CreateMediaUploadRequest request, ) async { final data = await _request( '/api/v1/media/uploads', method: 'POST', body: request.toJson(), ); return MediaUploadCredentials.fromJson(_asMap(data)); } @override Future completeMediaUpload(String assetId) async { // 幂等由服务端保证:已 ready 重复 confirm 返回 200 同一 asset。 final data = await _request( '/api/v1/media/uploads/$assetId/complete', method: 'POST', ); return MediaAsset.fromJson(_asMap(data)); } // ---- posts ---- @override Future createPost(CreatePostRequest request) async { final data = await _request( '/api/v1/posts', method: 'POST', body: request.toJson(), idempotent: true, ); return Post.fromJson(_asMap(data)); } @override Future getPost(String postId) async { final data = await _request('/api/v1/posts/$postId'); return Post.fromJson(_asMap(data)); } @override Future updatePost(String postId, UpdatePostRequest request) async { final data = await _request( '/api/v1/posts/$postId', method: 'PATCH', body: request.toJson(), ); return Post.fromJson(_asMap(data)); } @override Future deletePost(String postId) async { await _request('/api/v1/posts/$postId', method: 'DELETE'); } @override Future> listMyPosts({ int? limit, String? cursor, PostStatus? status, }) async { final data = await _request( '/api/v1/me/posts', query: { 'limit': ?limit, 'cursor': ?cursor, if (status != null) 'status': status.name, }, ); return CursorPage.fromJson(_asMap(data), Post.fromJson); } // ---- feed ---- @override Future> getFeed({int? limit, String? cursor}) async { final data = await _request( '/api/v1/feed', query: {'limit': ?limit, 'cursor': ?cursor}, ); return CursorPage.fromJson(_asMap(data), FeedCard.fromJson); } // ---- comments ---- @override Future> listComments( String postId, { int? limit, String? cursor, }) async { final data = await _request( '/api/v1/posts/$postId/comments', query: {'limit': ?limit, 'cursor': ?cursor}, ); return CursorPage.fromJson(_asMap(data), PostComment.fromJson); } @override Future createComment( String postId, CreateCommentRequest request, ) async { final data = await _request( '/api/v1/posts/$postId/comments', method: 'POST', body: request.toJson(), idempotent: true, ); return PostComment.fromJson(_asMap(data)); } @override Future deleteComment(String commentId) async { // 顶层短路径先例:commentId 全局唯一。 await _request('/api/v1/comments/$commentId', method: 'DELETE'); } // ---- interactions ---- @override Future likePost(String postId) async { final data = await _request('/api/v1/posts/$postId/like', method: 'PUT'); return LikeState.fromJson(_asMap(data)); } @override Future unlikePost(String postId) async { final data = await _request('/api/v1/posts/$postId/like', method: 'DELETE'); return LikeState.fromJson(_asMap(data)); } @override Future bookmarkPost(String postId) async { final data = await _request( '/api/v1/posts/$postId/bookmark', method: 'PUT', ); return BookmarkState.fromJson(_asMap(data)); } @override Future unbookmarkPost(String postId) async { final data = await _request( '/api/v1/posts/$postId/bookmark', method: 'DELETE', ); return BookmarkState.fromJson(_asMap(data)); } @override Future> listMyBookmarks({ int? limit, String? cursor, }) async { final data = await _request( '/api/v1/me/bookmarks', query: {'limit': ?limit, 'cursor': ?cursor}, ); return CursorPage.fromJson(_asMap(data), FeedCard.fromJson); } // ---- follows ---- @override Future followUser(String userId) async { final data = await _request('/api/v1/users/$userId/follow', method: 'PUT'); return FollowState.fromJson(_asMap(data)); } @override Future unfollowUser(String userId) async { final data = await _request( '/api/v1/users/$userId/follow', method: 'DELETE', ); return FollowState.fromJson(_asMap(data)); } @override Future getFollowStats(String userId) async { final data = await _request('/api/v1/users/$userId/follow-stats'); return FollowStats.fromJson(_asMap(data)); } }