报修照片传到一半断网,重试为什么又传一遍?
报修单带三张照片,前两张已经上传,第三张断网。再次提交时把三张全传一次,会浪费已经完成的工作;如果单据其实已受理、只是响应丢了,还可能产生另一份维修申请。解决时要分别保存成功附件引用与固定提交意图,再按失败发生的阶段恢复。
先分清照片阶段和业务提交阶段
照片上传成功,只能说明服务端存在可引用的文件;维修单是否受理,取决于后续业务提交。客户端至少需要保存:本地附件与成功引用的映射、待提交的冻结载荷、同一次提交的标识,以及业务回执。
| 故障位置 | 已经确定什么 | 下一次应做什么 |
|---|---|---|
| 第二张上传失败 | 第一张已有引用 | 复用第一张,继续处理缺失引用 |
| 单据提交后超时 | 受理结果不确定 | 按原提交标识查回执 |
| 明确业务拒绝 | 该次提交未被接受 | 按拒绝原因修订;失效附件另处理 |
这属于“复用成功文件”,不是一个文件分成多块的断点续传。文件已经传完、但引用尚未落盘时断开,也仍可能重传;不能据此承诺整个链路每张照片只传一次。
每成功一张,就把引用写进可恢复状态
下面的完整示例用两个合成附件。第一张成功,第二张第一次失败,随后从真实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; }
}
- 点赞
- 收藏
- 关注作者
评论(0)