Skip to Content
Week 12📋 Week 12 阶段检查

Week 12 - 阶段检查

这一页检查 Week 12 是否真正收尾:边界能处理、体验有状态、测试能验证、文档能照做、版本能发布、交付能验收。

本周学习地图

阶段先掌握什么最终能写什么
边界空值、格式错误、负数、文件缺失能让程序不失控
体验空、保存中、错误、成功能让操作状态清楚
测试正常、边界、异常、回归能用测试固定规则
文档README、手册、排错、清单能让别人照着运行
发布版本号、Release、发布目录能生成交付产物
验收操作、期望、实际、结论能证明版本可交付

本周自测项目:最终交付包

把 Week 10-11 的项目整理成一个交付包,至少包含:

  • Release 发布目录。
  • README、用户手册、排错文档。
  • xUnit 测试结果。
  • 示例 JSON 和 CSV。
  • 验收清单和验收结果。
  • 已知问题和下一步计划。

过关标准:另一个人只看交付包,就能启动、使用、排错和判断版本是否通过。

上位机阶段验收:可演示、可安装、可继续扩展的毕业项目

本周目标: 冻结范围,完成测试、文档、版本、发布和最终验收。

必须亲手完成:

  1. 正常流程、故障流程、重启恢复全部通过
  2. 安装、操作和排错文档交给陌生人也能使用
  3. 发布 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 的收尾目标就达到了。