06-Flutter 鸿蒙实战 06:上传后处理流程,照片识别、故事生成和状态刷新

06. Flutter 鸿蒙实战 06:上传后处理流程,照片识别、故事生成和状态刷新

上传成功只是开始,后面还有处理接口、故事生成、照片活化和列表刷新。

上传后的链路

照片上传完成并不代表业务结束。这个项目的核心价值在于"老照片变成可讲述的故事",所以上传后还要触发后端处理:识别照片、提取年代地点人物、生成故事、尝试生成活化视频。前端需要清楚区分"上传中"和"处理中"。

上传页里两个状态分开:

dart 复制代码
bool _uploading = false;
bool _processing = false;

上传阶段显示具体百分比,处理阶段显示不确定进度:

dart 复制代码
if (_processing) ...[
  const SizedBox(height: 16),
  const LinearProgressIndicator(),
  const SizedBox(height: 8),
  const Text(
    '正在识别照片、生成故事并活化视频,请不要离开',
    textAlign: TextAlign.center,
  ),
]

为什么要分阶段

文件上传是客户端可感知的字节传输,进度可以计算;照片处理属于服务端任务,耗时取决于识别、故事生成和视频活化,前端不能伪造百分比。把这两个阶段混在一起,会让用户误以为卡在上传。

项目变更记录里有过真实问题:活化视频失败曾经导致整张照片处理失败。后来修正为故事生成成功后,活化失败只更新照片活化状态,不再删除照片和故事。这里可以沉淀出一个实现原则:非核心附加能力失败,不应该抹掉已经成功的主数据。

处理接口后的状态更新

上传成功后,页面会从返回结果里取出 photo,再触发处理流程并刷新列表。核心思想是:不要只依赖本地临时状态,最终要以服务端照片列表为准。

dart 复制代码
final response = await apiClient.uploadPhoto(
  bytes: bytes,
  filename: file.name,
  mimeType: mimeType,
  onProgress: (value) {
    if (mounted) setState(() => _progress = value);
  },
);
final photo = response['photo'];

处理完成后刷新上传列表,也可以让首页、故事页拿到新数据。项目里 StoriesRepository.clearCache() 在登录切换时清缓存,上传成功后也应该考虑相关页面刷新。

错误展示

上传页把异常转成用户可理解文案:

dart 复制代码
String _message(Object exception) => userFacingError(exception);

前端不要把后端错误全部吞掉,也不要用"操作失败"覆盖所有情况。比如文件太大、格式不支持、网络断开、处理失败,这些提示应该不同。用户能根据提示采取下一步动作。

和后端任务状态的关系

tasks.mdchanges.md 可以看到,项目围绕照片处理修过多次问题:活化模型参数、活化状态枚举、视频缓存、额度耗尽提示、403 链接失效等。这些问题最后都指向同一个原则:移动端页面只展示真实后端状态,不假装成功。

例如活化视频生成失败时,照片和故事仍然可以保存;视频额度耗尽时,详情页提示额度问题,而不是让用户重新上传照片。

处理流程图

上传页和后端处理链路必须放在一起理解。它不是单纯 Flutter UI,而是移动端与服务端长任务协作的完整场景。

处理阶段拆分

上传成功不等于处理成功

照片上传接口只说明原图已经到达服务器。后续识别、故事生成、活化视频都可能继续运行。前端如果把"上传成功"当成"全部完成",用户会在故事页看不到新内容,或者误以为处理卡住。

这个项目用 _processing 单独表达后端处理阶段,配合提示文案和刷新逻辑,让用户知道照片已经上传,服务端还在继续生成内容。

非核心能力失败不能删除主数据

项目真实修复过一个问题:活化视频失败导致整张照片处理失败。后续改成故事生成成功后,活化失败只更新照片活化状态,不删除照片和故事。这个处理方式可以拆成三条规则:

text 复制代码
主链路:照片保存 -> 信息识别 -> 故事生成
增强链路:照片活化 -> 视频缓存 -> 端内播放

增强链路失败时,不应该破坏主链路结果。移动端页面也要按这个边界展示状态。

长任务状态设计

长任务不能用一次请求概括

照片上传完成,只能说明原图已经传到服务器。后面的识别、故事生成、语音准备、视频活化都属于服务端处理链路。移动端如果把上传成功等同于全部完成,用户很容易在故事页看不到内容,误以为数据丢了。

比较清晰的状态语义应该这样拆:

text 复制代码
selected    本地已选择照片
uploading   字节正在上传
uploaded    原图到达服务端
processing  服务端正在生成业务内容
ready       故事或照片状态可展示
failed      某个阶段失败,需要明确提示

当前项目用 _processing 区分后端处理阶段。页面文案也对应这个阶段,告诉用户照片已经进入服务端处理,而不是简单显示"上传中"。

主数据和附加能力要隔离

项目曾经遇到过一个典型问题:活化视频失败导致整张照片处理失败。后面改成故事生成成功后,视频活化失败只更新照片活化状态,不删除照片和故事。这种处理方式更符合移动端业务优先级。

text 复制代码
主数据:照片、故事、年代、地点、人物
附加能力:语音、视频活化、缩略图增强

主数据成功后,附加能力失败不能抹掉主数据。否则用户会看到"明明上传成功了,最后什么都没有"的结果。正确做法是保留照片和故事,再在详情页展示视频状态或错误提示。

前端状态刷新要围绕真实接口

移动端不要自己猜服务端处理结果。上传完成后,应该通过照片列表、故事列表、照片详情接口刷新真实状态。

dart 复制代码
Future<void> _loadPhotos() async {
  final rows = await api.getJsonList('/api/photos');
  final photos = rows
      .whereType<Map<String, dynamic>>()
      .map(AlbumPhoto.fromJson)
      .toList();
  setState(() {
    _photos = photos;
    _photosLoading = false;
  });
}

这段逻辑看起来普通,但它保证页面展示的是服务端最终状态。识别服务是否完成、故事是否生成、视频状态是否失败,都应该从接口拿。

错误提示要分层

上传后处理链路里,错误不能全部归成"操作失败"。比较合理的提示层级如下:

text 复制代码
文件错误:格式不支持、体积过大
网络错误:断网、超时、服务器不可达
认证错误:token 失效、未登录
业务错误:识别失败、额度耗尽、视频生成失败
数据错误:接口结构异常、缺少必要字段

这些错误的处理动作不同。文件错误要重新选择,网络错误要重试,认证错误要回登录页,业务错误要展示具体状态,数据错误要查接口结构。提示越准确,用户越容易继续操作,开发者也更容易定位问题。

验证处理链路

完整验证不能只看上传接口返回 200,还要继续检查后续页面:

text 复制代码
1. 上传照片后照片列表出现新照片。
2. 故事列表能看到新生成的故事。
3. 故事详情里的年代、地点、人物字段有内容。
4. 视频活化失败时故事仍然保留。
5. 刷新页面后状态不回退。

这组验证能覆盖"上传成功但业务内容没落地"的问题。移动端项目做长任务时,最怕页面提前报成功,后面状态却跟不上;把状态刷新接到真实接口上,问题会少很多。

处理链路排查

页面文案要反映真实阶段

处理阶段的文案不能继续写"上传中"。上传和处理是两个动作,用户看到不同文案,才知道现在不是网络传输慢,而是服务端正在生成内容。

text 复制代码
正在上传 35%       字节仍在传输
正在处理照片       服务端已收到照片,正在生成内容
处理失败           服务端任务失败,需要重试或查看错误

文案准确会减少误解。用户如果以为还在上传,可能会重复点击或退出页面;知道已经进入处理阶段,就更容易等待或去别的页面。

状态刷新不要依赖固定等待

移动端常见错误是上传后 Future.delayed 几秒再刷新。这种写法不稳定,网络快时浪费时间,网络慢时仍然拿不到结果。更稳的是围绕接口状态刷新。

text 复制代码
上传接口返回 -> 刷新照片列表
照片详情接口 -> 查看识别字段和视频状态
故事列表接口 -> 查看故事是否生成
详情页轮询 -> 只针对仍在处理中的视频状态

当前项目里,上传页刷新照片列表,故事详情页针对活化状态做轮询。不同页面只刷新自己需要的数据,避免全局乱刷。

失败隔离的代码边界

处理链路里可以把失败分为"阻断型"和"非阻断型"。

text 复制代码
阻断型:照片上传失败、故事主内容生成失败
非阻断型:视频活化失败、缩略图加载失败、高清语音生成失败

阻断型失败要阻止进入下一阶段,非阻断型失败只更新局部状态。这个边界能避免一个附加功能失败导致整条业务链路回滚。

数据一致性检查

处理完成后,至少要检查三个接口的数据是否一致:

text 复制代码
/api/photos              上传列表里有照片
/api/stories             故事列表里有对应故事
/api/photos/{id}         照片详情里有年代、地点、人物、视频状态

如果照片存在但故事不存在,问题在故事生成或列表刷新;如果故事存在但照片详情缺字段,问题在识别结果或元数据保存;如果视频状态失败但故事存在,属于非阻断失败,不应该删除故事。

处理流程问题定位表

现象 优先检查 对应接口或文件 处理方向
上传成功但故事列表没有内容 故事生成是否完成 /api/stories 刷新故事列表,检查服务端处理日志
照片有但元数据为空 识别结果是否写入 /api/photos/{id} 检查年代、地点、人物字段
视频失败导致故事消失 主数据和附加能力是否耦合 后端处理链路、详情页状态 视频失败只更新视频状态,不删除故事
页面一直显示处理中 状态刷新是否依赖真实接口 upload_page.dartstory_detail_page.dart 通过详情接口刷新状态,不靠固定等待

长任务最怕状态含糊。上传、识别、故事生成、视频活化是不同阶段,页面要分别表达。只有这样,用户才能知道现在是网络慢、服务端忙,还是某个附加能力失败。

处理状态字段设计

上传后的处理链路最好不要只用一个布尔值表示。更清楚的做法是给照片维护独立的处理字段,页面根据字段组合展示状态。

text 复制代码
photo.status            uploaded / analyzing / ready / failed
photo.storyStatus       pending / generated / failed
photo.animationStatus   none / processing / ready / failed
photo.errorMessage      最近一次失败原因

页面展示时可以按优先级判断:

dart 复制代码
String processingLabel(PhotoInfo photo) {
  if (photo.status == 'failed') return '照片识别失败';
  if (photo.storyStatus == 'pending') return '故事生成中';
  if (photo.animationStatus == 'processing') return '视频活化中';
  if (photo.animationStatus == 'failed') return '故事已生成,视频暂不可用';
  return '处理完成';
}

这样比"上传成功/失败"更精确。照片识别、故事生成、视频活化可以分别失败、分别恢复,页面也能给出不同提示。

相关推荐
Helen_cai2 小时前
HarmonyOS ArkTS 实战:实现一个校园课程表与考勤管理应用
华为·harmonyos
天天爱吃肉82182 小时前
# 商用车多体动力学整车仿真学习笔记(开篇总览)
人工智能·笔记·python·功能测试·学习·汽车
风华圆舞2 小时前
鸿蒙HarmonyOS 多模态输入实战 —— 图片与文档输入智谱/DeepSeek
开发语言·人工智能·华为·php·harmonyos·arkts·arkui
一起努力啊~2 小时前
DataWhale组队学习笔记--llm-algo-leetcode(三)
笔记·学习·llm
云空3 小时前
《Three.js 可操控3D魔方(键盘转动+打乱+复原)》
javascript·学习·游戏·3d·three.js
kdxiaojie3 小时前
Linux 驱动研究 —— V4L2 (3)
linux·运维·笔记·学习
AD02273 小时前
31-后台任务重复执行-用唯一任务键和租约防止并发
harmonyos·arkts·鸿蒙开发
我是小a3 小时前
HarmonyOS API 24 List组件与数据渲染技术详解
华为·harmonyos·鸿蒙
我是小a4 小时前
HarmonyOS API 24 交互事件与动画效果技术详解
华为·harmonyos·鸿蒙