报修照片传到一半断网,重试为什么又传一遍?

举报
yd_299226675 发表于 2026/10/27 10:04:13 2026/10/27
【摘要】 报修单带三张照片,前两张已经上传,第三张断网。再次提交时把三张全传一次,会浪费已经完成的工作;如果单据其实已受理、只是响应丢了,还可能产生另一份维修申请。解决时要分别保存成功附件引用与固定提交意图,再按失败发生的阶段恢复。

报修单带三张照片,前两张已经上传,第三张断网。再次提交时把三张全传一次,会浪费已经完成的工作;如果单据其实已受理、只是响应丢了,还可能产生另一份维修申请。解决时要分别保存成功附件引用与固定提交意图,再按失败发生的阶段恢复。

先分清照片阶段和业务提交阶段

照片上传成功,只能说明服务端存在可引用的文件;维修单是否受理,取决于后续业务提交。客户端至少需要保存:本地附件与成功引用的映射、待提交的冻结载荷、同一次提交的标识,以及业务回执。

故障位置 已经确定什么 下一次应做什么
第二张上传失败 第一张已有引用 复用第一张,继续处理缺失引用
单据提交后超时 受理结果不确定 按原提交标识查回执
明确业务拒绝 该次提交未被接受 按拒绝原因修订;失效附件另处理

这属于“复用成功文件”,不是一个文件分成多块的断点续传。文件已经传完、但引用尚未落盘时断开,也仍可能重传;不能据此承诺整个链路每张照片只传一次。

每成功一张,就把引用写进可恢复状态

下面的完整示例用两个合成附件。第一张成功,第二张第一次失败,随后从真实JSON文件重新读取状态。再执行时跳过已有引用,结果是第一张调用一次、第二张调用两次。它模拟重新加载状态,没有执行系统断电或进程强杀。

示例把文件写入显式输出目录。文件写入接口的使用方式可参见Microsoft .NET文件写入文档。单次WriteAllText不等于崩溃安全事务;需要保证掉电一致性时,要设计临时文件替换、同步刷盘或事务存储,再做故障注入。

业务响应丢失时,保留原来的提交意图

同一次确认提交应使用同一标识。客户端进入“结果未知”后先查原回执;拿到原申请号再结束这次处理,不能仅凭超时创建新标识。服务端仍须按该标识做可靠的去重和回执保存,客户端变量本身不能保证服务端幂等。

AssetaGuard手持端报修实现也区分附件引用落盘、确认提交和结果未知回查。本文独立示例只抽取这个分阶段恢复思路,传输器和服务端回执用内存替身,没有把产品代码作为公开附件。

用可核对的输出判断恢复行为

本机合成示例得到:firstPhotoCalls=1、secondPhotoCalls=2、businessRecords=1。程序还断言阶段顺序为附件持久化、附件失败、附件持久化、提交结果未知、回执找到。这些结果证明指定示例分支,不代表真实网络带宽、设备耐久性或产品全部报修流程。

可先运行示例,再在自己的提交链路中注入“附件部分成功”和“单据成功但响应丢失”两类故障,对照调用次数与回执。

完整独立示例

以下程序使用合成数据,不调用产品接口,不需要账号、数据库或模型服务。准备 .NET 8或兼容SDK,将项目文件与程序放入同一目录。运行命令为 dotnet run --project . -- example-output,输出文件写入指定目录。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>
using System.Text.Json;
using System.Globalization;

var root = args.Length > 0 ? Path.GetFullPath(args[0]) : Path.Combine(Environment.CurrentDirectory, "example-output");
Directory.CreateDirectory(root);
var results = new Dictionary<string, object>();
CheckAttachments();
File.WriteAllText(Path.Combine(root, "results.json"), JsonSerializer.Serialize(results, new JsonSerializerOptions { WriteIndented = true }));
void Require(bool value, string code) { if (!value) throw new InvalidOperationException(code); }

void CheckAttachments()
{
    var path = Path.Combine(root, "attachment-state.json");
    var state = new UploadState();
    var uploads = new Dictionary<string, int>();
    var receipts = new Dictionary<string, string>();
    var events = new List<string>();
    void Save() => File.WriteAllText(path, JsonSerializer.Serialize(state));
    string Upload(string key, bool fail)
    {
        uploads[key] = uploads.GetValueOrDefault(key) + 1;
        if (fail) throw new IOException("INJECTED_PHOTO_FAILURE");
        return "blob-" + key;
    }
    foreach (var photo in new[] { "p1", "p2" })
    {
        try { state.Uploaded[photo] = Upload(photo, photo == "p2"); Save(); events.Add("photo.persisted"); }
        catch (IOException) { events.Add("photo.failed"); break; }
    }
    // 新对象从真实文件重读,模拟下一次启动;它不是进程kill或断电实测。
    state = JsonSerializer.Deserialize<UploadState>(File.ReadAllText(path))!;
    foreach (var photo in new[] { "p1", "p2" })
    {
        if (state.Uploaded.ContainsKey(photo)) continue;
        state.Uploaded[photo] = Upload(photo, false); Save(); events.Add("photo.persisted");
    }
    var originalAttempt = "intent-001";
    state.AttemptId = originalAttempt; state.Status = "Submitting"; Save();
    receipts[originalAttempt] = "request-001"; // 服务端受理成功,但客户端没有拿到响应。
    state.Status = "ResultUnknown"; Save(); events.Add("submit.result_unknown");
    state = JsonSerializer.Deserialize<UploadState>(File.ReadAllText(path))!;
    Require(receipts.TryGetValue(state.AttemptId!, out var requestId), "RECEIPT_NOT_FOUND");
    state.Status = "Submitted"; state.RequestId = requestId; Save(); events.Add("receipt.found");
    Require(uploads["p1"] == 1 && uploads["p2"] == 2, "PHOTO_COUNTS");
    Require(receipts.Count == 1 && state.AttemptId == originalAttempt, "DUPLICATE_INTENT");
    Require(events.SequenceEqual(new[] { "photo.persisted", "photo.failed", "photo.persisted", "submit.result_unknown", "receipt.found" }), "EVENT_ORDER");
    // 只输出稳定阶段与次数,正文/本地文件名/账号不进入日志。
    results["A2"] = new { passed = true, firstPhotoCalls = 1, secondPhotoCalls = 2, businessRecords = 1, events };
    Console.WriteLine("PASS A2 firstPhotoCalls=1 secondPhotoCalls=2 businessRecords=1");
}

public sealed class UploadState
{
    public Dictionary<string, string> Uploaded { get; set; } = new();
    public string? AttemptId { get; set; }
    public string Status { get; set; } = "Draft";
    public string? RequestId { get; set; }
}
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。