Skip to Content
Week 11Day 7 - 复盘与巩固

Day 7 - 复盘与巩固

建议用时:120-150 分钟

你将学会什么

  • 怎么把 Week 11 的可靠性功能串成主线
  • 权限、审计、导入导出、后台任务、设置、发布候选分别解决什么问题
  • 怎么用一个程序复盘这些概念
  • 怎么写本周最终验收清单

Week 11 的主题不是“多加几个功能”,而是让项目变得更可靠:谁能做、谁做过、数据怎么进出、长任务怎么跑、设置怎么保存、交付前怎么检查。

本页固定顺序

  1. 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
  2. 再学第二部分:把刚学的知识组合成一个完整例子。
  3. 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
  4. 最后做第四部分:先独立完成作业,再用完整答案检查。

学习衔接

上一页学习的是“发布候选”,今天继续学习“复盘与巩固”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。

今天的最低通过线

第一次学习不要求背完整页。完成下面 3 项,就可以继续:

  • 能用自己的话说明“复盘与巩固”解决什么问题。
  • 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
  • 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。

第一部分:先学原理和最小知识

这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。Week 11 的主线是可靠性。

Week 11 的主线

日期主题解决的问题
Day 1角色与权限谁能做什么
Day 2审计日志谁做过什么
Day 3导入导出数据怎么进出系统
Day 4后台任务长任务怎么不阻塞界面
Day 5设置与偏好用户配置怎么保存和恢复
Day 6发布候选交付前怎么检查
Day 7复盘把这些可靠性能力串起来

这些功能为什么属于一组

它们都不是单纯让界面更好看。

它们解决的是项目长期运行的问题:

  • 权限:避免不该做的人执行关键动作。
  • 审计:操作之后能追踪。
  • 导入导出:和外部文件交换数据。
  • 后台任务:耗时操作不让界面卡住。
  • 设置:用户偏好能保存。
  • 发布候选:交付前有标准。

本周最重要的判断

每个可靠性功能都要回答:

  1. 成功时记录什么?
  2. 失败时怎么提示?
  3. 失败时有没有可追踪信息?
  4. 用户能不能继续操作?
  5. 交付时怎么验证?

只写成功流程,不算可靠。

常见混乱点

混乱点正确理解
隐藏按钮就是权限错,服务层还要检查
普通日志就是审计错,审计记录业务动作
导入失败就直接停止不够好,要记录错误行
后台任务就是 async不够,还要状态、进度、取消
设置能保存就行不够,还要默认值和回退
打包成功就是发布不够,还要文档和验收

Week 11 项目交付常用操作总表

主题必须会的操作
权限UserRolePermissionNamesCan(...)
审计AuditEntryRecord(...)、按目标查询
CSVReadAllLinesWriteAllLinesSplitJoin
后台任务async TaskCancellationTokenSourceProgress
设置AppSettings、JSON 保存、默认值、重置
发布候选README、CHANGELOG、KNOWN-ISSUES、CHECKLIST
验证dotnet builddotnet testdotnet publish

第二部分:把知识组合成完整例子

今天用一个控制台程序复盘 Week 11。

它会做 5 件事:

  1. Viewer 删除被拒绝,Admin 删除成功,并写审计。
  2. 导入 CSV,统计成功和失败。
  3. 设置值非法时回到默认值。
  4. 后台任务报告进度。
  5. 输出发布候选检查项。

完整 Program.cs

using System; using System.Collections.Generic; using System.Linq; var auditLogs = new List<string>(); void TryDelete(Role role, string user, List<string> audit) { if (role != Role.Admin) { audit.Add($"{DateTimeOffset.UtcNow:HH:mm:ss} {user} Product.Delete denied"); Console.WriteLine($"{user} 没有删除权限。"); return; } audit.Add($"{DateTimeOffset.UtcNow:HH:mm:ss} {user} Product.Delete success"); Console.WriteLine($"{user} 删除成功。"); } async Task RunBackgroundJobAsync(Action<int> reportProgress) { for (int index = 1; index <= 5; index++) { await Task.Delay(80); reportProgress(index * 20); } } ImportSummary ImportProducts(IReadOnlyList<string> lines) { int successCount = 0; var errors = new List<string>(); var usedIds = new HashSet<int>(); for (int index = 1; index < lines.Count; index++) { int lineNumber = index + 1; string line = lines[index]; string[] parts = line.Split(','); if (parts.Length != 4) { errors.Add($"第 {lineNumber} 行:列数必须是 4。"); continue; } if (!int.TryParse(parts[0], out int id)) { errors.Add($"第 {lineNumber} 行:Id 必须是整数。"); continue; } if (!usedIds.Add(id)) { errors.Add($"第 {lineNumber} 行:Id 重复。"); continue; } if (string.IsNullOrWhiteSpace(parts[1])) { errors.Add($"第 {lineNumber} 行:Name 不能为空。"); continue; } if (!decimal.TryParse(parts[2], out decimal price) || price <= 0) { errors.Add($"第 {lineNumber} 行:Price 必须是大于 0 的数字。"); continue; } if (!int.TryParse(parts[3], out int stock) || stock < 0) { errors.Add($"第 {lineNumber} 行:Stock 必须是大于或等于 0 的整数。"); continue; } successCount++; } return new ImportSummary(successCount, errors); } AppSettings NormalizeSettings(AppSettings settings) { string theme = settings.Theme is "Light" or "Dark" ? settings.Theme : "Light"; int pageSize = settings.PageSize is >= 10 and <= 500 ? settings.PageSize : 50; string dataPath = string.IsNullOrWhiteSpace(settings.DataPath) ? "products.json" : settings.DataPath; return new AppSettings(theme, pageSize, dataPath); } TryDelete(Role.Viewer, "Alice", auditLogs); TryDelete(Role.Admin, "Cindy", auditLogs); Console.WriteLine("审计日志:"); foreach (string log in auditLogs) { Console.WriteLine(log); } string[] csvLines = { "Id,Name,Price,Stock", "1,Keyboard,199,10", "2,,59,30", "3,Monitor,abc,5", "4,Mouse,59,20" }; ImportSummary importSummary = ImportProducts(csvLines); Console.WriteLine($"导入成功:{importSummary.SuccessCount}"); Console.WriteLine($"导入失败:{importSummary.Errors.Count}"); foreach (string error in importSummary.Errors) { Console.WriteLine(error); } AppSettings settings = NormalizeSettings(new AppSettings("Blue", 999, "")); Console.WriteLine($"设置:Theme={settings.Theme}, PageSize={settings.PageSize}, DataPath={settings.DataPath}"); string[] releaseChecklist = { "版本号已更新", "主流程已回归", "README 已写运行方式", "CHANGELOG 已写变更", "已知问题已记录" }; Console.WriteLine("发布候选检查:"); foreach (string item in releaseChecklist) { Console.WriteLine($"[x] {item}"); } public enum Role { Viewer, Admin } public sealed record ImportSummary(int SuccessCount, IReadOnlyList<string> Errors); public sealed record AppSettings(string Theme, int PageSize, string DataPath);

正常运行结果

Alice 没有删除权限。 Cindy 删除成功。 审计日志: ... 导入成功:2 导入失败:2 第 3 行:Name 不能为空。 第 4 行:Price 必须是大于 0 的数字。 设置:Theme=Light, PageSize=50, DataPath=products.json 后台任务进度:20% ... 后台任务进度:100% 发布候选检查: [x] 版本号已更新 ...

第三部分:跟着敲代码

从这里开始动手。用控制台项目复盘 Week 11。

第 1 步:创建项目

dotnet new console -n Week11Review cd Week11Review

打开 Program.cs,清空原内容。

第 2 步:写权限和审计

var auditLogs = new List<string>(); TryDelete(Role.Viewer, "Alice", auditLogs); TryDelete(Role.Admin, "Cindy", auditLogs);

解释:

  • Viewer 删除失败。
  • Admin 删除成功。
  • 两次都写审计。

第 3 步:写导入汇总

ImportSummary importSummary = ImportProducts(csvLines); Console.WriteLine($"导入成功:{importSummary.SuccessCount}"); Console.WriteLine($"导入失败:{importSummary.Errors.Count}");

解释:

  • 成功和失败都要统计。
  • 错误要有行号和原因。

第 4 步:写设置回退

AppSettings settings = NormalizeSettings(new AppSettings("Blue", 999, "")); Console.WriteLine($"设置:Theme={settings.Theme}, PageSize={settings.PageSize}, DataPath={settings.DataPath}");

解释:

  • Blue 非法,回到 Light
  • 999 超范围,回到 50
  • 空路径回到 products.json

第 5 步:写后台进度

await RunBackgroundJobAsync(progress => { Console.WriteLine($"后台任务进度:{progress}%"); });

解释:

  • 任务逐步报告进度。
  • 页面里可以把这个进度绑定到进度条。

第 6 步:写发布候选检查

string[] releaseChecklist = { "版本号已更新", "主流程已回归", "README 已写运行方式", "CHANGELOG 已写变更", "已知问题已记录" };

解释:

  • 发布候选不是只有程序文件。
  • 文档和验收清单也是交付物。

常见错误和修法

错误为什么错修法
复盘只看新增功能忽略了权限、日志、任务、设置这些稳定性能力按功能增强和稳定性增强分开检查
权限只测管理员普通角色可能越权管理员和普通角色都要测
后台任务不测失败真实运行时容易卡住测成功、失败、取消三种情况
设置保存后不重启验证不知道是否真正持久化关闭重开应用再检查设置
发布候选没有下一步计划无法进入正式发布写清阻断问题、非阻断问题、正式版条件

小白重复敲写训练

复盘日把权限、审计和设置组成一个最小闭环。

训练 1:权限判断

bool canDelete = currentRole == UserRole.Admin; Console.WriteLine(canDelete);

训练 2:操作后记录审计

if (canDelete) { products.Remove(product); WriteAudit("admin", "Delete", product.Name); }

训练 3:保存用户偏好

var settings = new AppSettings { Theme = "Dark" }; await File.WriteAllTextAsync( "settings.json", JsonSerializer.Serialize(settings));

第三遍自己补上无权限时的用户提示和审计日志。

每日小测

做完本页后,用这 5 题检查是否真的掌握。

1. 判断题

本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。

答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。

2. 填空题

本页主题是:复盘与巩固。今天至少要掌握的 3 个点是:

1. 怎么把 Week 11 的可靠性功能串成主线 2. 权限、审计、导入导出、后台任务、设置、发布候选分别解决什么问题 3. 怎么用一个程序复盘这些概念

答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。

3. 流程题

遇到本页相关功能时,先按什么顺序处理?

答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。

4. 找错误题

如果本页代码运行失败,第一步应该做什么?

答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。

5. 改需求题

在本页完整例子跑通后,至少改一个小需求。

可选改法:

  • 改一个字段名称。
  • 多加一个校验条件。
  • 多输出一行结果。
  • 把固定数据改成用户输入。
  • 把一次处理改成多条数据处理。

答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:怎么把 Week 11 的可靠性功能串成主线。

上位机专项练习

本周过关标准是项目具备现场交付需要的权限、审计、批量配置、稳定轮询和发布流程。

下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。

专项例子 1:复习权限验收

操作员: 查看、启停、确认报警 工程师: 操作员权限 + 修改设备和点位 管理员: 工程师权限 + 用户与审计管理

运行结果或界面效果:

三种角色边界明确

改动任务: 用三个测试账号逐项验证。

专项例子 2:复习故障演练

1. 拔掉 PLC-01 网线 2. 确认 PLC-02 继续更新 3. 确认 PLC-01 显示离线 4. 插回网线 5. 确认自动重连并恢复数据

运行结果或界面效果:

通信故障有完整恢复流程

改动任务: 记录每一步实际耗时。

专项例子 3:复习交付证据

证据目录: - screenshots/ - logs/ - test-results/ - release-notes.md - acceptance-checklist.md

运行结果或界面效果:

验收不是口头说完成,而是留下证据

改动任务: 实际创建并填充这些文件。

第四部分:作业完整答案

作业要求:写一个 Week 11 复盘程序,覆盖权限、审计、导入错误、设置回退、后台进度和发布候选检查。

答案文件:Program.cs

using System; using System.Collections.Generic; using System.Linq; var auditLogs = new List<string>(); void TryDelete(Role role, string user, List<string> audit) { if (role != Role.Admin) { audit.Add($"{DateTimeOffset.UtcNow:HH:mm:ss} {user} Product.Delete denied"); Console.WriteLine($"{user} 没有删除权限。"); return; } audit.Add($"{DateTimeOffset.UtcNow:HH:mm:ss} {user} Product.Delete success"); Console.WriteLine($"{user} 删除成功。"); } async Task RunBackgroundJobAsync(Action<int> reportProgress) { for (int index = 1; index <= 5; index++) { await Task.Delay(80); reportProgress(index * 20); } } ImportSummary ImportProducts(IReadOnlyList<string> lines) { int successCount = 0; var errors = new List<string>(); var usedIds = new HashSet<int>(); for (int index = 1; index < lines.Count; index++) { int lineNumber = index + 1; string line = lines[index]; string[] parts = line.Split(','); if (parts.Length != 4) { errors.Add($"第 {lineNumber} 行:列数必须是 4。"); continue; } if (!int.TryParse(parts[0], out int id)) { errors.Add($"第 {lineNumber} 行:Id 必须是整数。"); continue; } if (!usedIds.Add(id)) { errors.Add($"第 {lineNumber} 行:Id 重复。"); continue; } if (string.IsNullOrWhiteSpace(parts[1])) { errors.Add($"第 {lineNumber} 行:Name 不能为空。"); continue; } if (!decimal.TryParse(parts[2], out decimal price) || price <= 0) { errors.Add($"第 {lineNumber} 行:Price 必须是大于 0 的数字。"); continue; } if (!int.TryParse(parts[3], out int stock) || stock < 0) { errors.Add($"第 {lineNumber} 行:Stock 必须是大于或等于 0 的整数。"); continue; } successCount++; } return new ImportSummary(successCount, errors); } AppSettings NormalizeSettings(AppSettings settings) { string theme = settings.Theme is "Light" or "Dark" ? settings.Theme : "Light"; int pageSize = settings.PageSize is >= 10 and <= 500 ? settings.PageSize : 50; string dataPath = string.IsNullOrWhiteSpace(settings.DataPath) ? "products.json" : settings.DataPath; return new AppSettings(theme, pageSize, dataPath); } TryDelete(Role.Viewer, "Alice", auditLogs); TryDelete(Role.Admin, "Cindy", auditLogs); Console.WriteLine("审计日志:"); foreach (string log in auditLogs) { Console.WriteLine(log); } string[] csvLines = { "Id,Name,Price,Stock", "1,Keyboard,199,10", "2,,59,30", "3,Monitor,abc,5", "4,Mouse,59,20" }; ImportSummary importSummary = ImportProducts(csvLines); Console.WriteLine($"导入成功:{importSummary.SuccessCount}"); Console.WriteLine($"导入失败:{importSummary.Errors.Count}"); foreach (string error in importSummary.Errors) { Console.WriteLine(error); } AppSettings settings = NormalizeSettings(new AppSettings("Blue", 999, "")); Console.WriteLine($"设置:Theme={settings.Theme}, PageSize={settings.PageSize}, DataPath={settings.DataPath}"); string[] releaseChecklist = { "版本号已更新", "主流程已回归", "README 已写运行方式", "CHANGELOG 已写变更", "已知问题已记录" }; Console.WriteLine("发布候选检查:"); foreach (string item in releaseChecklist) { Console.WriteLine($"[x] {item}"); } public enum Role { Viewer, Admin } public sealed record ImportSummary(int SuccessCount, IReadOnlyList<string> Errors); public sealed record AppSettings(string Theme, int PageSize, string DataPath);

验收结果

运行后必须看到:

  1. Alice 删除失败。
  2. Cindy 删除成功。
  3. 审计日志有 denied 和 success。
  4. 导入成功 2 行。
  5. 导入失败 2 行。
  6. 设置回退为 Light50products.json
  7. 后台任务进度到 100%。
  8. 发布候选检查项全部输出。

为什么这个答案是对的

这个答案把 Week 11 的可靠性主线串起来了:

能力程序里的体现
权限TryDelete 检查角色
审计auditLogs 记录成功和拒绝
导入ImportProducts 统计成功失败
设置NormalizeSettings 处理非法值
后台任务RunBackgroundJobAsync 报告进度
发布候选releaseChecklist 输出交付检查

Week 11 的目标是让项目从“能操作”升级到“能控制、能追踪、能恢复、能交付”。