新增:community feature 数据层——契约 v1.3.0 十九操作全覆盖 + ToggleSync 乐观更新状态机(T3-12)
CI / flutter-gates (push) Successful in 2m26s
CI / flutter-gates (push) Successful in 2m26s
- community_models:community/media 域 DTO 逐字段照冻结契约手写 JSON 映射 (Post/FeedCard/PostComment/AuthorSummary 降级形态/媒体两步上传凭据等), 未知枚举抛 FormatException 暴露契约漂移;CursorPage 上移 core 复用 - community_repository:13 路径 19 操作全覆盖;createPost/createComment 必带 Idempotency-Key(每次逻辑提交换新键、刷新重放同键);点赞/收藏/ 关注走 PUT/DELETE 语义幂等 - community_exceptions:v1.3.0 新增 9 码 + 40902 共码类型化异常映射 - toggle_sync:乐观翻转 + 快照回滚 + 单飞合并最终意图 + 代次守卫, 点赞/收藏共用一套参数化状态机,权威终态对账收敛 - community_controller:Feed 多页缓存 + 首屏四态 + 尾部加载三态 + 游标拼接 + 刷新代次丢弃旧尾页;详情副本与卡片互动状态同源;reset 清态 - app.dart 装配:community 服务分端口直连(:8084),共享 TokenRefresher, 登出同步 reset - 测试 286 → 347(模型映射 / 19 操作线路 / 错误映射 / 竞态序列全覆盖) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import 'package:patbond_flutter/core/network/api_exception.dart';
|
||||
|
||||
/// community / media 域类型化业务异常(契约 v1.3.0 定型的 9 个新错误码,
|
||||
/// 20 号收口报告 §1;40902 乐观锁与 pets 域共码,本域映射为独立类型)。
|
||||
/// 全部继承 [ApiBusinessException],既有按基类捕获的通用错误处理不受影响。
|
||||
|
||||
/// 40301:对可见帖子/评论无相应操作权限(改删他人已发布帖、
|
||||
/// 删他人可见评论——含帖主删他人评论,D3-7 首版不做)。
|
||||
final class PostAccessDeniedException extends ApiBusinessException {
|
||||
const PostAccessDeniedException({required super.message})
|
||||
: super(code: ApiCodes.postAccessDenied);
|
||||
}
|
||||
|
||||
/// 40403:帖子不存在 / 已软删 / hidden/archived(含作者)/ 他人 draft
|
||||
/// (防枚举,全部情况响应完全一致;互动路径上含作者本人草稿)。
|
||||
final class PostNotFoundException extends ApiBusinessException {
|
||||
const PostNotFoundException({required super.message})
|
||||
: super(code: ApiCodes.postNotFound);
|
||||
}
|
||||
|
||||
/// 40404:评论不存在、已删或所属帖子不可见(防枚举合并)。
|
||||
final class CommentNotFoundException extends ApiBusinessException {
|
||||
const CommentNotFoundException({required super.message})
|
||||
: super(code: ApiCodes.commentNotFound);
|
||||
}
|
||||
|
||||
/// 40405:media asset 不存在、非本人所有或已删(防枚举合并)。
|
||||
final class MediaAssetNotFoundException extends ApiBusinessException {
|
||||
const MediaAssetNotFoundException({required super.message})
|
||||
: super(code: ApiCodes.mediaNotFound);
|
||||
}
|
||||
|
||||
/// 40406:目标用户不存在或已注销(合并不泄露成因)。
|
||||
final class CommunityUserNotFoundException extends ApiBusinessException {
|
||||
const CommunityUserNotFoundException({required super.message})
|
||||
: super(code: ApiCodes.communityUserNotFound);
|
||||
}
|
||||
|
||||
/// 40902:帖子编辑乐观锁版本冲突(version 过期)。
|
||||
/// 客户端处理:刷新详情取新 version 后重提。
|
||||
final class PostVersionConflictException extends ApiBusinessException {
|
||||
const PostVersionConflictException({required super.message})
|
||||
: super(code: ApiCodes.versionConflict);
|
||||
}
|
||||
|
||||
/// 40905:同 Idempotency-Key 不同 payload(规范化 request_hash 不符)。
|
||||
/// 客户端每次逻辑提交应换新键,重试间保持不变。
|
||||
final class IdempotencyMismatchException extends ApiBusinessException {
|
||||
const IdempotencyMismatchException({required super.message})
|
||||
: super(code: ApiCodes.idempotencyKeyMismatch);
|
||||
}
|
||||
|
||||
/// 42203:引用了本人所有但非 ready(uploading/failed)状态的 asset。
|
||||
final class MediaNotReadyException extends ApiBusinessException {
|
||||
const MediaNotReadyException({required super.message})
|
||||
: super(code: ApiCodes.mediaNotReady);
|
||||
}
|
||||
|
||||
/// 42204:自关注(仅 PUT;自取关走 DELETE 的 200 幂等 no-op)。
|
||||
final class SelfFollowException extends ApiBusinessException {
|
||||
const SelfFollowException({required super.message})
|
||||
: super(code: ApiCodes.selfFollow);
|
||||
}
|
||||
|
||||
/// 42205:上传状态不允许确认——对象未上传(保持 uploading 可重试)、
|
||||
/// 大小/类型不符(置 failed 终态)、failed 态再确认(已 ready 幂等 200 除外)。
|
||||
final class MediaUploadStateException extends ApiBusinessException {
|
||||
const MediaUploadStateException({required super.message})
|
||||
: super(code: ApiCodes.mediaUploadStateInvalid);
|
||||
}
|
||||
|
||||
/// 把通用业务异常按 community/media 域错误码升格为类型化异常;
|
||||
/// 未覆盖的码(40000 参数错误、40401 宠物防枚举等)原样返回,沿用通用处理。
|
||||
ApiBusinessException mapCommunityBusinessException(ApiBusinessException error) {
|
||||
return switch (error.code) {
|
||||
ApiCodes.postAccessDenied => PostAccessDeniedException(
|
||||
message: error.message,
|
||||
),
|
||||
ApiCodes.postNotFound => PostNotFoundException(message: error.message),
|
||||
ApiCodes.commentNotFound => CommentNotFoundException(
|
||||
message: error.message,
|
||||
),
|
||||
ApiCodes.mediaNotFound => MediaAssetNotFoundException(
|
||||
message: error.message,
|
||||
),
|
||||
ApiCodes.communityUserNotFound => CommunityUserNotFoundException(
|
||||
message: error.message,
|
||||
),
|
||||
ApiCodes.versionConflict => PostVersionConflictException(
|
||||
message: error.message,
|
||||
),
|
||||
ApiCodes.idempotencyKeyMismatch => IdempotencyMismatchException(
|
||||
message: error.message,
|
||||
),
|
||||
ApiCodes.mediaNotReady => MediaNotReadyException(message: error.message),
|
||||
ApiCodes.selfFollow => SelfFollowException(message: error.message),
|
||||
ApiCodes.mediaUploadStateInvalid => MediaUploadStateException(
|
||||
message: error.message,
|
||||
),
|
||||
_ => error,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user