新增:首页 Feed 接入真实数据——四态/尾部三态/聚合曝光埋点,社区 demo 消亡第一页(T3-14)
CI / flutter-gates (push) Successful in 2m41s
CI / flutter-gates (push) Successful in 2m41s
- home_page Feed segment 改接 CommunityController:骨架屏首载、空态 CTA、 错误横幅重试、下拉刷新(失败保留旧列表 + SnackBar)、触底游标翻页 (失败态只走显式重试,不随滚动风暴自动重打) - PostCard 三形态(单图通栏 4:3 / 多图折叠封面 +N 角标 / 纯文字 6 行)+ PostMediaGrid(列数规则 + ink 80% scrim 精算)+ LikeButton(error 族修订 Colors.red,T3-14 纯展示禁用态,ToggleSync 接线留 T3-15/16)+ FeedSkeleton (呼吸动效尊重减弱动态设置);降级作者统一「宠友」占位 - SignedNetworkImage:预签名 URL 缓存 key 剥离 X-Amz-* 签名参数, RemoteImage 全仓换用(同图不同签名命中同一缓存条目) - feed 域埋点:feed_viewed 聚合曝光(浏览段开/结算于切 Tab、切分段、 退后台、销毁;≥50% 可见 ≥500ms 段内按帖去重,postId 不上报)+ feed_load_failed(refresh/load_more 双路,会话失效不报) - integration_test/feed_live_test.dart:compose 六容器 Linux 桌面真链路 实测入口(PATBOND_FEED_LIVE=1 门控,默认跳过) - 测试 379 → 421(+42):四态/尾部三态/翻页不丢不重/降级作者/曝光结算 时机/缓存 key/事件属性全覆盖;analyze 0,format 无 diff Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
|
||||
/// 预签名 URL 的图片内存缓存 key:剥离 SigV4 签名参数(`X-Amz-*`,
|
||||
/// 大小写不敏感),保留其余 query。
|
||||
///
|
||||
/// 媒体 URL 每次响应现签(TTL 1 小时),同一对象两次响应的完整 URL
|
||||
/// 必然不同;若按完整 URL 作缓存 key,同图会反复未命中、重复下载。
|
||||
/// 对象路径唯一标识 MinIO 对象,剥签名后即稳定 key。
|
||||
String presignedImageCacheKey(String url) {
|
||||
final uri = Uri.tryParse(url);
|
||||
if (uri == null || uri.queryParameters.isEmpty) return url;
|
||||
final kept = <String, String>{};
|
||||
var stripped = false;
|
||||
uri.queryParameters.forEach((key, value) {
|
||||
if (key.toLowerCase().startsWith('x-amz-')) {
|
||||
stripped = true;
|
||||
} else {
|
||||
kept[key] = value;
|
||||
}
|
||||
});
|
||||
if (!stripped) return url;
|
||||
// Uri.replace 的 null 语义是「保留原值」,全剥空时手工截断 query。
|
||||
if (kept.isEmpty) return url.substring(0, url.indexOf('?'));
|
||||
return uri.replace(queryParameters: kept).toString();
|
||||
}
|
||||
|
||||
/// 以剥签名 key 判等的网络图 provider:同对象的不同签名 URL 命中同一
|
||||
/// [ImageCache] 条目;未命中时仍以完整签名 URL 发起请求(委托
|
||||
/// [NetworkImage] 加载,其为 factory-only 接口,无法直接继承)。
|
||||
class SignedNetworkImage extends ImageProvider<SignedNetworkImage> {
|
||||
SignedNetworkImage(this.url, {this.scale = 1.0})
|
||||
: cacheKey = presignedImageCacheKey(url);
|
||||
|
||||
final String url;
|
||||
final double scale;
|
||||
|
||||
/// 剥离签名参数后的稳定缓存 key。
|
||||
final String cacheKey;
|
||||
|
||||
@override
|
||||
Future<SignedNetworkImage> obtainKey(ImageConfiguration configuration) =>
|
||||
SynchronousFuture<SignedNetworkImage>(this);
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(
|
||||
SignedNetworkImage key,
|
||||
ImageDecoderCallback decode,
|
||||
) {
|
||||
final delegate = NetworkImage(key.url, scale: key.scale);
|
||||
return delegate.loadImage(delegate, decode);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
return other is SignedNetworkImage &&
|
||||
other.cacheKey == cacheKey &&
|
||||
other.scale == scale;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(cacheKey, scale);
|
||||
|
||||
@override
|
||||
String toString() => 'SignedNetworkImage("$url", cacheKey: "$cacheKey")';
|
||||
}
|
||||
Reference in New Issue
Block a user