Week 12 - 阶段检查
这一页检查 Week 12 是否真正收尾:边界能处理、体验有状态、测试能验证、文档能照做、版本能发布、交付能验收。
本周学习地图
| 阶段 | 先掌握什么 | 最终能写什么 |
|---|---|---|
| 边界 | 空值、格式错误、负数、文件缺失 | 能让程序不失控 |
| 体验 | 空、保存中、错误、成功 | 能让操作状态清楚 |
| 测试 | 正常、边界、异常、回归 | 能用测试固定规则 |
| 文档 | README、手册、排错、清单 | 能让别人照着运行 |
| 发布 | 版本号、Release、发布目录 | 能生成交付产物 |
| 验收 | 操作、期望、实际、结论 | 能证明版本可交付 |
本周自测项目:最终交付包
把 Week 10-11 的项目整理成一个交付包,至少包含:
- Release 发布目录。
- README、用户手册、排错文档。
- xUnit 测试结果。
- 示例 JSON 和 CSV。
- 验收清单和验收结果。
- 已知问题和下一步计划。
过关标准:另一个人只看交付包,就能启动、使用、排错和判断版本是否通过。
上位机阶段验收:可演示、可安装、可继续扩展的毕业项目
本周目标: 冻结范围,完成测试、文档、版本、发布和最终验收。
必须亲手完成:
- 正常流程、故障流程、重启恢复全部通过
- 安装、操作和排错文档交给陌生人也能使用
- 发布 1.0.0 包、变更记录和 SHA256
过关标准: 你能独立演示项目并解释分层,下一步可替换模拟通信为串口、TCP 或 Modbus。
不要只看答案。新建一个空项目重做一次,运行成功后再故意改坏一处并自己排错。
本周流程图
边界和异常处理
-> UI 状态反馈
-> xUnit 测试固定规则
-> README 和用户手册说明操作
-> RELEASE_NOTES 说明版本变化
-> dotnet publish 生成发布目录
-> 验收清单记录操作、期望、实际、结论
-> 最终交付包前半部分:本周原理地图
Week 12 的主线是:把项目从“能演示”整理成“能交付”。
能演示,只要主流程跑通。能交付,需要正常流程、错误流程、文档、版本、发布包和验收记录都能对上。
7 天内容对应的能力
| 天数 | 主题 | 最终要能做到什么 |
|---|---|---|
| Day 1 | 边界与异常 | 空值、格式错误、负数、文件缺失不会让程序失控 |
| Day 2 | 体验优化 | 空状态、保存中、错误提示、按钮禁用都清楚 |
| Day 3 | 测试补齐 | 用 xUnit 验证正常、边界、异常、回归场景 |
| Day 4 | 文档完善 | README、用户手册、排错文档、检查清单能照做 |
| Day 5 | 版本与发布 | 版本号明确,Release 发布目录能生成 |
| Day 6 | 验收与交付 | 交付包结构完整,验收记录可追踪 |
| Day 7 | 复盘与下一步 | 能把 12 周内容整理成继续练项目的路线 |
本周最重要的判断标准
| 能力 | 不合格表现 | 合格表现 |
|---|---|---|
| 边界 | 只处理正常输入 | 空名称、错误价格、负库存都有明确处理 |
| 体验 | 点击后界面没反馈 | 保存中、成功、失败、空列表都有状态 |
| 测试 | 只靠手动点界面 | 业务规则有自动测试 |
| 文档 | 只写“运行项目” | 写清环境、命令、使用方式、常见问题 |
| 发布 | 只在开发目录运行 | 能生成 Release 发布目录 |
| 验收 | 只说“功能正常” | 每条验收都有操作和期望结果 |
| 交付 | 只给程序文件 | 程序、文档、示例数据、验收记录一起交付 |
最低通过线
达到下面 8 条,Week 12 才算真正完成:
- 能说清项目交付前为什么要处理边界。
- 能写出商品名称、价格、库存的校验规则。
- 能在 Avalonia 页面里区分普通状态、错误状态、保存中状态、空状态。
- 能写至少 5 个 xUnit 测试,覆盖正常和错误场景。
- 能写 README,别人能照着启动项目。
- 能执行
dotnet publish -c Release -o ./artifacts/ProductManager-1.0.0。 - 能写出验收清单,包含操作步骤和期望结果。
- 能说明当前版本能否交付,以及原因。
如果某一项不会,回到哪一页
| 卡住的问题 | 回看页面 |
|---|---|
| 不知道怎么处理空值、错误格式、文件缺失 | Day 1 |
| 页面点击后没有反馈,不知道怎么设计状态 | Day 2 |
| 不知道怎么写测试 | Day 3 |
| 不知道 README 和用户手册写什么 | Day 4 |
| 不知道版本号和 publish 命令 | Day 5 |
| 不知道怎么验收和交付 | Day 6 |
| 不知道下一步继续练什么 | Day 7 |
下半部分:最终验收练习
下面写一个控制台程序,模拟检查一个 ProductManager-1.0.0 交付包。
这个程序会检查:
- 发布目录里有没有程序文件。
- 文档文件是否存在。
- README 里是否写了启动命令。
- 示例 JSON 是否能解析。
- 验收结果是否写了“允许交付”。
这不是替代真实手工验收,而是训练你用代码把交付检查流程固定下来。
第 1 步:新建控制台项目
dotnet new console -n ReleaseAudit
cd ReleaseAudit第 2 步:替换 Program.cs
把 Program.cs 替换成下面完整代码。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
string releaseRoot = Path.Combine(
Environment.CurrentDirectory,
"release",
"ProductManager-1.0.0");
PrepareSampleReleasePackage(releaseRoot);
List<CheckResult> results = new()
{
CheckFileExists(releaseRoot, "app/ProductManager.dll"),
CheckFileExists(releaseRoot, "docs/README.md"),
CheckFileExists(releaseRoot, "docs/USER_GUIDE.md"),
CheckFileExists(releaseRoot, "docs/TROUBLESHOOTING.md"),
CheckFileExists(releaseRoot, "docs/RELEASE_NOTES_1.0.0.md"),
CheckFileExists(releaseRoot, "sample-data/products.json"),
CheckFileExists(releaseRoot, "checks/ACCEPTANCE_TEST.md"),
CheckFileExists(releaseRoot, "checks/ACCEPTANCE_RESULT.md"),
CheckTextContains(releaseRoot, "docs/README.md", "dotnet run"),
CheckJsonArray(releaseRoot, "sample-data/products.json", 3),
CheckTextContains(releaseRoot, "checks/ACCEPTANCE_RESULT.md", "验收结论:允许交付")
};
PrintResults(results);
int passedCount = results.Count(result => result.Passed);
Console.WriteLine();
Console.WriteLine($"通过:{passedCount}/{results.Count}");
Console.WriteLine(passedCount == results.Count ? "结论:允许交付" : "结论:暂不交付");
static void PrepareSampleReleasePackage(string releaseRoot)
{
WriteFile(releaseRoot, "app/ProductManager.dll", "fake app file for audit practice");
WriteFile(releaseRoot, "docs/README.md", "# ProductManager\n\n启动命令:dotnet run\n");
WriteFile(releaseRoot, "docs/USER_GUIDE.md", "# 用户手册\n\n新增、编辑、删除、搜索商品。\n");
WriteFile(releaseRoot, "docs/TROUBLESHOOTING.md", "# 常见问题\n\n启动失败时先执行 dotnet --version。\n");
WriteFile(releaseRoot, "docs/RELEASE_NOTES_1.0.0.md", "# ProductManager 1.0.0 发布说明\n\n发布日期:2026-06-12\n");
WriteFile(releaseRoot, "checks/ACCEPTANCE_TEST.md", "# 验收清单\n\n- [ ] 新增商品\n- [ ] 搜索商品\n");
WriteFile(releaseRoot, "checks/ACCEPTANCE_RESULT.md", "# 验收结果\n\n验收结论:允许交付\n");
WriteFile(
releaseRoot,
"sample-data/products.json",
"""
[
{ "id": 1, "name": "Keyboard", "price": 199, "stock": 10 },
{ "id": 2, "name": "Mouse", "price": 99, "stock": 20 },
{ "id": 3, "name": "Monitor", "price": 899, "stock": 5 }
]
""");
}
static void WriteFile(string releaseRoot, string relativePath, string content)
{
string path = ToFullPath(releaseRoot, relativePath);
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllText(path, content);
}
static CheckResult CheckFileExists(string releaseRoot, string relativePath)
{
string path = ToFullPath(releaseRoot, relativePath);
bool exists = File.Exists(path);
return new CheckResult(
$"文件存在:{relativePath}",
exists,
exists ? "通过" : "文件不存在");
}
static CheckResult CheckTextContains(string releaseRoot, string relativePath, string expectedText)
{
string path = ToFullPath(releaseRoot, relativePath);
if (!File.Exists(path))
{
return new CheckResult(
$"文本包含:{relativePath}",
false,
"文件不存在");
}
string content = File.ReadAllText(path);
bool contains = content.Contains(expectedText, StringComparison.OrdinalIgnoreCase);
return new CheckResult(
$"文本包含:{relativePath} -> {expectedText}",
contains,
contains ? "通过" : "没有找到指定文字");
}
static CheckResult CheckJsonArray(string releaseRoot, string relativePath, int minimumCount)
{
string path = ToFullPath(releaseRoot, relativePath);
if (!File.Exists(path))
{
return new CheckResult(
$"JSON 数组:{relativePath}",
false,
"文件不存在");
}
try
{
string json = File.ReadAllText(path);
using JsonDocument document = JsonDocument.Parse(json);
if (document.RootElement.ValueKind != JsonValueKind.Array)
{
return new CheckResult(
$"JSON 数组:{relativePath}",
false,
"根节点不是数组");
}
int count = document.RootElement.GetArrayLength();
bool enoughRows = count >= minimumCount;
return new CheckResult(
$"JSON 数组:{relativePath}",
enoughRows,
enoughRows ? $"通过,数据 {count} 条" : $"数据只有 {count} 条");
}
catch (JsonException ex)
{
return new CheckResult(
$"JSON 数组:{relativePath}",
false,
$"JSON 格式错误:{ex.Message}");
}
}
static string ToFullPath(string releaseRoot, string relativePath)
{
string normalizedPath = relativePath.Replace('/', Path.DirectorySeparatorChar);
return Path.Combine(releaseRoot, normalizedPath);
}
static void PrintResults(IEnumerable<CheckResult> results)
{
foreach (CheckResult result in results)
{
string status = result.Passed ? "通过" : "失败";
Console.WriteLine($"{status} - {result.Name} - {result.Message}");
}
}
public sealed record CheckResult(
string Name,
bool Passed,
string Message);第 3 步:运行
dotnet run正确结果应该看到:
通过:11/11
结论:允许交付第 4 步:故意制造一个失败
把这行:
WriteFile(releaseRoot, "docs/README.md", "# ProductManager\n\n启动命令:dotnet run\n");临时改成:
WriteFile(releaseRoot, "docs/README.md", "# ProductManager\n\n启动命令:启动应用\n");再运行:
dotnet run这时 README 检查应该失败,因为程序找不到 dotnet run。
验证完以后改回正确版本。
这一步说明:验收标准必须具体。只写“有 README”不够,还要检查 README 里有没有关键启动信息。
阶段作业完整答案
作业要求
写一份 Week 12 最终交付复盘,回答:
- 本周做了哪些收尾工作。
- 哪些内容已经通过。
- 哪些内容还需要继续改。
- 当前版本能不能交付。
完整答案:WEEK12_FINAL_REVIEW.md
# Week 12 最终交付复盘
## 1. 本周完成的收尾工作
- 补齐边界处理:商品名称、价格、库存、文件缺失。
- 补齐体验状态:空列表、保存中、错误提示、保存成功。
- 补齐测试:商品规则测试、商品服务测试。
- 补齐文档:README、用户手册、排错文档、发布检查清单。
- 补齐版本材料:VERSION、CHANGELOG、RELEASE_NOTES。
- 补齐交付材料:示例数据、验收清单、验收结果、交接说明。
## 2. 已通过内容
- 商品名称为空会被拦截。
- 价格小于等于 0 会被拦截。
- 库存小于 0 会被拦截。
- 保存中按钮不可重复点击。
- xUnit 测试能验证商品规则。
- README 写清运行环境和启动步骤。
- Release 发布目录能生成。
- 交付包包含程序、文档、示例数据和验收记录。
## 3. 还需要继续改的内容
- CSV 导入失败时,后续显示失败行号。
- 删除商品后,后续增加撤销能力。
- JSON 保存后续可以升级成 SQLite。
- 桌面端后续可以改为调用 Web API。
## 4. 当前版本是否能交付
允许交付。
原因:
- 主流程已经跑通。
- 关键错误输入已经处理。
- 测试、文档、版本、发布和验收材料已经补齐。
- 已知问题不阻塞当前版本使用,并且已经写入文档。
## 5. 下一步计划
1. 重做商品管理项目,确认基础能力稳定。
2. 把 JSON 保存换成 SQLite。
3. 用 ASP.NET Core 做商品 Web API。
4. 让 Avalonia 桌面端调用 Web API。正确结果
完成 Week 12 后,你应该能做三件事:
- 从代码角度判断项目是否稳定。
- 从文档角度判断别人能否接手。
- 从交付角度判断版本是否能发布。
如果这三件事都能说明白,Week 12 的收尾目标就达到了。