1441f0148f
CI / flutter-gates (push) Successful in 2m30s
- MediaUploader:选图→压缩(80→60 降质阶梯,10 MiB 超限拒绝)→createUpload →预签名 PUT 直传(进度回调)→confirm→ready assetId 交付;并发槽位 2、 顺序即 position、单图失败不拖垮整批;凭据过期预检与存储侧 403 各自动 换新凭据一次;失败重试复用压缩产物、从 createUpload 全新开始 - 孤儿防护:未 confirm 的 assetId 只以管线局部变量存在,快照 assetId 与 ready 态断言绑定,buildAttachRequests 非全员 ready 即抛 StateError - 线路修正:media 两步上传端点由 user 服务(:8082 MediaController)提供, T3-12 误挂 community 客户端(:8084 无 media 路由),补 mediaApi 分端口 直连,compose 真链路实测证实 - UploadProgressOverlay 四态进度层(05 号规范 §3.3);直传层本地 HttpServer 三线路测试(200/403/断连);compose 冒烟测试 (PATBOND_MEDIA_SMOKE=1 启用,默认跳过) - 新依赖:image_picker、flutter_image_compress(03 号评估选型) - flutter test 379 全绿(基线 347,+32)、analyze 0、format 无 diff Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
54 lines
1.8 KiB
Dart
54 lines
1.8 KiB
Dart
import 'dart:typed_data';
|
|
|
|
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
|
import 'package:patbond_flutter/features/community/media_picking.dart';
|
|
|
|
/// 压缩产物(直传本体)。mimeType 与直传 PUT 的 Content-Type、
|
|
/// createUpload 登记值三处必须一致(Content-Type 已签进预签名签名)。
|
|
class CompressedMediaImage {
|
|
const CompressedMediaImage({required this.bytes, required this.mimeType});
|
|
|
|
final Uint8List bytes;
|
|
final String mimeType;
|
|
|
|
int get byteSize => bytes.length;
|
|
}
|
|
|
|
/// 压缩抽象:quality 由 MediaUploader 的降质阶梯驱动(80 → 60),
|
|
/// 单测注入假实现控制产物大小。
|
|
abstract class MediaImageCompressor {
|
|
Future<CompressedMediaImage> compress(
|
|
PickedMediaImage source, {
|
|
required int quality,
|
|
});
|
|
}
|
|
|
|
/// 基于 flutter_image_compress 的原生压缩实现(03 号评估 §4.1 选型)。
|
|
///
|
|
/// 策略(03 号 §4.1 + 13 号报告偏差 #6):长边 ≤2048 重采样、统一转码
|
|
/// JPEG(服务端 mime 白名单不收 HEIC,客户端统一出 jpeg);
|
|
/// autoCorrectionAngle 矫正方向后不保留 EXIF——顺带剥离 GPS 定位隐私,
|
|
/// **不要开 keepExif**。
|
|
class NativeMediaImageCompressor implements MediaImageCompressor {
|
|
const NativeMediaImageCompressor({this.maxLongEdge = 2048});
|
|
|
|
final int maxLongEdge;
|
|
|
|
@override
|
|
Future<CompressedMediaImage> compress(
|
|
PickedMediaImage source, {
|
|
required int quality,
|
|
}) async {
|
|
final bytes = await FlutterImageCompress.compressWithList(
|
|
source.bytes,
|
|
minWidth: maxLongEdge,
|
|
minHeight: maxLongEdge,
|
|
quality: quality,
|
|
format: CompressFormat.jpeg,
|
|
autoCorrectionAngle: true,
|
|
keepExif: false,
|
|
);
|
|
return CompressedMediaImage(bytes: bytes, mimeType: 'image/jpeg');
|
|
}
|
|
}
|