Files
customer-im-client-dev/apps/im_app/lib/data/remote/upload_file_request.dart
pp-bot e715a0673b feat(chat): 发收消息全量实现 (#25~#28)
- 移除 @riverpod/@freezed 注解依赖,全部改为手写 Provider(无需 build_runner)
  · LoginState 改为纯 Dart,LoginViewModel/ThemeViewModel/ChatViewModel 改为 Notifier
  · UserNotifier 改为 FamilyAsyncNotifier<User?,int>,mini_app_provider 改为手写 Provider
  · 15 个 StreamProvider/StreamProvider.family 从 @riverpod 迁移至手写

- 发送消息(#25)
  · SendMessageRequest/SendMessageResponse DTO
  · SendMessageUseCase:乐观写入 DB → HTTP POST → 更新 Chat 摘要

- 接收消息 WS(#26)
  · WsMessageService:监听 mode2 WS 帧 → HTTP 补拉 → DB 写入 → Chat 更新
  · FetchHistoryRequest/FetchHistoryResponse DTO(GET /app/api/chat/history)
  · FetchHistoryUseCase:拉取 → insertOrReplaceAll

- DI 装配(chat_service_providers.dart)
  · wsMessageServiceProvider、sendMessageUseCaseProvider、fetchHistoryUseCaseProvider

- 聊天列表页(#27)
  · ChatListViewModel(Notifier<void>)+ chat_page.dart 真实会话列表 UI
  · ListTile:头像首字母、最新消息摘要、未读角标、时间格式化

- 聊天详情页(#28)
  · ChatDetailViewModel(FamilyNotifier<ChatDetailState,int>)+ chat_detail_page.dart
  · 消息气泡(自己/他人分左右)、底部输入框、发送状态与错误提示

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 23:16:44 +09:00

162 lines
5.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:typed_data';
import 'package:json_annotation/json_annotation.dart';
import 'package:networks_sdk/networks_sdk.dart';
import 'package:im_app/core/foundation/api_paths.dart';
part 'upload_file_request.g.dart';
/// # /upload/file — 文件上传Upload 请求示例)
///
/// 演示两种上传模式:
///
/// ## 模式 A: FormData 上传到自有后端
/// 适用于后端直接接收文件的场景。
/// 使用 [UploadFileRequest] — path 为相对路径SDK 自动拼 baseURL。
///
/// ## 模式 B: 二进制上传到 S3 presigned URL
/// 适用于先向后端获取 presigned URL再直接上传到 S3 的场景。
/// 使用 [S3UploadRequest] — path 为完整 URLoverride decodeResponse。
///
/// ## Upload 与普通请求的区别
///
/// | 普通请求 | Upload 请求 |
/// |---------|-----------|
/// | `toJson()` → JSON body | `uploadData` → FormData / Uint8List |
/// | `requestType: request` | `requestType: upload` |
/// | `parameters` 有值 | `parameters` 返回 null |
/// | 标准 `{ code, msg, data }` 响应 | 可能需要 override `decodeResponse` |
// ─────────────────────────────────────────────
// Response DTO
// ─────────────────────────────────────────────
/// 文件上传接口的业务响应数据(对应服务端 `data` 字段)。纯 Dart 类,无需任何注解。
class UploadResult {
final String url;
@JsonKey(name: 'file_id')
final String fileId;
const UploadResult({required this.url, required this.fileId});
factory UploadResult.fromJson(Map<String, dynamic> json) => UploadResult(
url: json['url'] as String? ?? '',
fileId: json['file_id'] as String? ?? '',
);
}
// ═════════════════════════════════════════════
// 模式 A: FormData 上传到自有后端
// ═════════════════════════════════════════════
/// FormData 上传请求
///
/// 上传到自有后端 `/upload/file`,响应为标准 `{ code, message, data }` 格式。
/// 无需 override `decodeResponse`。
@ApiRequest(
path: ApiPaths.uploadFile,
method: HttpMethod.post,
responseType: UploadResult,
requestType: ApiRequestType.upload,
)
class UploadFileRequest extends ApiRequestable<UploadResult>
with _$UploadFileRequestApi {
final String filePath;
final String? fileName;
UploadFileRequest({required this.filePath, this.fileName});
@override
Map<String, dynamic> toJson() => {};
/// FormData — SDK 通过 uploadData 获取上传数据
@override
Object? get uploadData {
return FormData.fromMap({
'file': MultipartFile.fromFileSync(filePath, filename: fileName),
});
}
}
// ═════════════════════════════════════════════
// 模式 B: 二进制上传到 S3 presigned URL
// ═════════════════════════════════════════════
/// S3 presigned URL 上传响应
class S3UploadResponse {
final bool success;
final String? message;
const S3UploadResponse({this.success = true, this.message});
}
/// S3 presigned URL 上传请求
///
/// 特点:
/// - path 为完整的 presigned URLSDK 检测到 http 开头不拼 baseURL
/// - uploadData 为 Uint8List 二进制数据
/// - 自定义 headersContent-Type: application/octet-stream
/// - override decodeResponse — S3 返回 204 No Content 或 XML不是标准响应格式
class S3UploadRequest extends ApiRequestable<S3UploadResponse> {
final Uint8List data;
final String presignedURL;
S3UploadRequest({required this.data, required this.presignedURL});
@override
String get path => presignedURL;
@override
HttpMethod get method => HttpMethod.put;
@override
ApiRequestType get requestType => ApiRequestType.upload;
@override
Map<String, String>? get customHeaders => {
'Content-Type': 'application/octet-stream',
};
@override
Map<String, dynamic> toJson() => {};
/// 二进制数据
@override
Object? get uploadData => data;
/// S3 响应不走标准 { code, message, data } 格式,需要自定义解码
///
/// 可能的响应:
/// - 204 No Content空 body→ 成功
/// - 200 + XML body → 成功
/// - 200 + JSON body → 尝试解码
@override
S3UploadResponse? decodeResponse(Response response) {
// 空响应或 2xx 状态码 → 成功
if (response.data == null ||
(response.data is List && (response.data as List).isEmpty)) {
return const S3UploadResponse(success: true);
}
// JSON 响应 → 尝试解码
if (response.data is Map<String, dynamic>) {
final json = response.data as Map<String, dynamic>;
return S3UploadResponse(
success: true,
message: json['message'] as String?,
);
}
// 2xx 状态码 → 成功
if (response.statusCode != null &&
response.statusCode! >= 200 &&
response.statusCode! < 300) {
return const S3UploadResponse(success: true);
}
return const S3UploadResponse(success: true);
}
}