Day 7 - 复盘与巩固
建议用时:120-150 分钟
你将学会什么
- 怎么把 Week 11 的可靠性功能串成主线
- 权限、审计、导入导出、后台任务、设置、发布候选分别解决什么问题
- 怎么用一个程序复盘这些概念
- 怎么写本周最终验收清单
Week 11 的主题不是“多加几个功能”,而是让项目变得更可靠:谁能做、谁做过、数据怎么进出、长任务怎么跑、设置怎么保存、交付前怎么检查。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“发布候选”,今天继续学习“复盘与巩固”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“复盘与巩固”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。Week 11 的主线是可靠性。
Week 11 的主线
| 日期 | 主题 | 解决的问题 |
|---|---|---|
| Day 1 | 角色与权限 | 谁能做什么 |
| Day 2 | 审计日志 | 谁做过什么 |
| Day 3 | 导入导出 | 数据怎么进出系统 |
| Day 4 | 后台任务 | 长任务怎么不阻塞界面 |
| Day 5 | 设置与偏好 | 用户配置怎么保存和恢复 |
| Day 6 | 发布候选 | 交付前怎么检查 |
| Day 7 | 复盘 | 把这些可靠性能力串起来 |
这些功能为什么属于一组
它们都不是单纯让界面更好看。
它们解决的是项目长期运行的问题:
- 权限:避免不该做的人执行关键动作。
- 审计:操作之后能追踪。
- 导入导出:和外部文件交换数据。
- 后台任务:耗时操作不让界面卡住。
- 设置:用户偏好能保存。
- 发布候选:交付前有标准。
本周最重要的判断
每个可靠性功能都要回答:
- 成功时记录什么?
- 失败时怎么提示?
- 失败时有没有可追踪信息?
- 用户能不能继续操作?
- 交付时怎么验证?
只写成功流程,不算可靠。
常见混乱点
| 混乱点 | 正确理解 |
|---|---|
| 隐藏按钮就是权限 | 错,服务层还要检查 |
| 普通日志就是审计 | 错,审计记录业务动作 |
| 导入失败就直接停止 | 不够好,要记录错误行 |
后台任务就是 async | 不够,还要状态、进度、取消 |
| 设置能保存就行 | 不够,还要默认值和回退 |
| 打包成功就是发布 | 不够,还要文档和验收 |
Week 11 项目交付常用操作总表
| 主题 | 必须会的操作 |
|---|---|
| 权限 | UserRole、PermissionNames、Can(...) |
| 审计 | AuditEntry、Record(...)、按目标查询 |
| CSV | ReadAllLines、WriteAllLines、Split、Join |
| 后台任务 | async Task、CancellationTokenSource、Progress |
| 设置 | AppSettings、JSON 保存、默认值、重置 |
| 发布候选 | README、CHANGELOG、KNOWN-ISSUES、CHECKLIST |
| 验证 | dotnet build、dotnet test、dotnet publish |
第二部分:把知识组合成完整例子
今天用一个控制台程序复盘 Week 11。
它会做 5 件事:
- Viewer 删除被拒绝,Admin 删除成功,并写审计。
- 导入 CSV,统计成功和失败。
- 设置值非法时回到默认值。
- 后台任务报告进度。
- 输出发布候选检查项。
完整 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);验收结果
运行后必须看到:
- Alice 删除失败。
- Cindy 删除成功。
- 审计日志有 denied 和 success。
- 导入成功 2 行。
- 导入失败 2 行。
- 设置回退为
Light、50、products.json。 - 后台任务进度到 100%。
- 发布候选检查项全部输出。
为什么这个答案是对的
这个答案把 Week 11 的可靠性主线串起来了:
| 能力 | 程序里的体现 |
|---|---|
| 权限 | TryDelete 检查角色 |
| 审计 | auditLogs 记录成功和拒绝 |
| 导入 | ImportProducts 统计成功失败 |
| 设置 | NormalizeSettings 处理非法值 |
| 后台任务 | RunBackgroundJobAsync 报告进度 |
| 发布候选 | releaseChecklist 输出交付检查 |
Week 11 的目标是让项目从“能操作”升级到“能控制、能追踪、能恢复、能交付”。