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

Week 10 - 阶段检查

这一页用来确认 Week 10 是否真正掌握:能从需求出发,写出数据层、服务层、主列表、编辑和搜索过滤的完整业务流程。

本周学习地图

阶段先掌握什么最终能写什么
需求范围、字段、规则、验收能把需求写成可实现清单
数据层模型、Repository、JSON能保存和读取商品
服务层校验、业务结果、错误消息能集中处理规则
主列表加载、选择、空状态能显示多条商品
编辑草稿、保存、取消、删除能安全修改商品
搜索全部数据和可见数据能组合过滤条件

本周自测项目:商品管理 MVP

从空 Avalonia 项目开始做最小商品管理系统,至少包含:

  • Product 模型。
  • ProductRepository 保存 JSON。
  • ProductService 校验名称、价格、库存。
  • 商品列表、新增、编辑、删除。
  • 关键词搜索和分类过滤。
  • 错误提示、空状态、保存成功提示。

过关标准:能解释 View、ViewModel、Service、Repository 每层放什么。

上位机阶段验收:上位机主项目第一版

本周目标: 打通需求、模型、轮询、报警、仪表盘、详情和搜索筛选。

必须亲手完成:

  1. 模拟 3 台设备持续产生点位数据
  2. 首页摘要和详情实时更新
  3. 制造越限值后生成并显示报警

过关标准: 从启动采集到查看详情和报警形成完整闭环,单台失败不影响其他设备。

不要只看答案。新建一个空项目重做一次,运行成功后再故意改坏一处并自己排错。

本周流程图

需求文档 -> Product 模型 -> Repository 读写 JSON -> Service 校验名称、价格、库存 -> ViewModel 保存输入、列表、选中项 -> View 绑定输入框、按钮、列表 -> 用户完成新增、编辑、删除、搜索

前半部分:本周原理地图

Week 10 的主线是:

把一个商品管理需求,拆成数据、规则和界面三层,再做出能新增、编辑、搜索、删除的最小业务系统。

这一周不是只练控件。真正要掌握的是:需求怎么落到字段,字段怎么落到模型,规则怎么放到服务层,界面怎么绑定状态。

能力地图

能力解决的问题最小判断
需求范围防止功能失控能写清做什么和不做什么
数据模型定义商品长什么样能写出 Product 字段
数据层封装保存和读取能写 Repository
服务层集中业务规则能校验名称、价格、库存
主列表显示和选择多条数据能绑定 ObservableCollection
详情编辑修改当前选中商品能用编辑草稿保存和取消
搜索过滤从全部数据筛出可见数据能区分全部数据和可见数据

分层判断

应该放什么不应该放什么
View / XAML控件、布局、绑定不写业务规则
ViewModel页面状态、命令、消息不直接处理文件细节
Service业务校验、业务动作不写控件布局
Repository保存、读取、查询不判断价格是否大于 0
Model数据字段不处理按钮点击

如果一个价格校验写在 XAML 里,位置就错了。

如果一个 JSON 文件路径写在按钮事件里,位置也错了。

本周必须掌握的 7 个点

  1. 需求文档要写“做什么”和“不做什么”。
  2. Product 要有 IdNamePriceStockCreatedAtUpdatedAt
  3. Repository 负责保存和读取。
  4. Service 负责校验和业务动作。
  5. SelectedProduct 表示当前选中的一条数据。
  6. 编辑表单要用草稿字段,不要直接污染原对象。
  7. 搜索过滤要保留全部数据,只刷新可见结果。

Week 10 必须会用的操作

主题必须会的写法
需求MVP 范围、字段、业务规则、验收标准
ModelProductIdNamePriceStock
RepositoryGetAllFindByIdAddUpdateDelete
JSONJsonSerializer.SerializeDeserializeFile.ReadAllTextWriteAllText
ServiceCreateProductRequestServiceResult、校验和查重
ViewModel 列表ObservableCollection<ProductItem>
选中项SelectedProduct
编辑草稿EditNameEditPriceTextEditStockText
搜索WhereContainsToLower
过滤OnlyInStockVisibleProducts

下半部分:阶段检查完整答案

下面是一份完整阶段检查程序。它会验证 Week 10 的核心业务流程:

  • 新增合法商品。
  • 拦住空名称。
  • 新增无库存商品。
  • 搜索关键字。
  • 过滤有库存商品。
  • 编辑商品。
  • 删除商品。

检查题:Program.cs

using System; using System.Collections.Generic; using System.Linq; var service = new ProductService(); ServiceResult keyboard = service.Create("Keyboard", "199", "10"); Print(keyboard); ServiceResult emptyName = service.Create("", "199", "10"); Print(emptyName); ServiceResult monitor = service.Create("Monitor", "899", "0"); Print(monitor); Console.WriteLine($"搜索 key:{service.Search("key", false).Count} 条"); Console.WriteLine($"仅显示有库存:{service.Search("", true).Count} 条"); if (keyboard.ProductId is Guid keyboardId) { ServiceResult updated = service.Update(keyboardId, "Keyboard Pro", "209", "8"); Print(updated); } Console.WriteLine($"搜索 pro:{service.Search("pro", false).Count} 条"); if (keyboard.ProductId is Guid productId) { ServiceResult deleted = service.Delete(productId); Print(deleted); } Console.WriteLine($"删除后总数:{service.Search("", false).Count} 条"); void Print(ServiceResult result) { Console.WriteLine($"{result.Success} - {result.Message}"); } public sealed class ProductService { private readonly List<Product> products = new(); public ServiceResult Create(string nameText, string priceText, string stockText) { ServiceResult? error = ValidateInput(nameText, priceText, stockText, out string name, out decimal price, out int stock); if (error is not null) { return error; } DateTime now = DateTime.Now; var product = new Product { Id = Guid.NewGuid(), Name = name, Price = price, Stock = stock, CreatedAt = now, UpdatedAt = now }; products.Add(product); return ServiceResult.Ok("商品新增成功。", product.Id); } public ServiceResult Update(Guid? id, string nameText, string priceText, string stockText) { if (id is null) { return ServiceResult.Fail("请先选择要编辑的商品。"); } Product? product = products.FirstOrDefault(item => item.Id == id.Value); if (product is null) { return ServiceResult.Fail("要编辑的商品不存在。"); } ServiceResult? error = ValidateInput(nameText, priceText, stockText, out string name, out decimal price, out int stock); if (error is not null) { return error; } product.Name = name; product.Price = price; product.Stock = stock; product.UpdatedAt = DateTime.Now; return ServiceResult.Ok("商品编辑成功。", product.Id); } public ServiceResult Delete(Guid? id) { if (id is null) { return ServiceResult.Fail("请先选择要删除的商品。"); } Product? product = products.FirstOrDefault(item => item.Id == id.Value); if (product is null) { return ServiceResult.Fail("要删除的商品不存在。"); } products.Remove(product); return ServiceResult.Ok("商品删除成功。"); } public List<Product> Search(string keyword, bool onlyInStock) { string trimmedKeyword = keyword.Trim(); IEnumerable<Product> query = products; if (!string.IsNullOrWhiteSpace(trimmedKeyword)) { query = query.Where(product => product.Name.Contains(trimmedKeyword, StringComparison.OrdinalIgnoreCase)); } if (onlyInStock) { query = query.Where(product => product.Stock > 0); } return query.ToList(); } private static ServiceResult? ValidateInput( string nameText, string priceText, string stockText, out string name, out decimal price, out int stock) { name = nameText.Trim(); price = 0; stock = 0; if (string.IsNullOrWhiteSpace(name)) { return ServiceResult.Fail("商品名称不能为空。"); } if (name.Length > 50) { return ServiceResult.Fail("商品名称不能超过 50 个字符。"); } if (!decimal.TryParse(priceText, out price)) { return ServiceResult.Fail("价格必须是数字。"); } if (price <= 0) { return ServiceResult.Fail("价格必须大于 0。"); } if (!int.TryParse(stockText, out stock)) { return ServiceResult.Fail("库存必须是整数。"); } if (stock < 0) { return ServiceResult.Fail("库存不能小于 0。"); } return null; } } public sealed class Product { public Guid Id { get; set; } public string Name { get; set; } = ""; public decimal Price { get; set; } public int Stock { get; set; } public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } } public sealed class ServiceResult { public bool Success { get; init; } public string Message { get; init; } = ""; public Guid? ProductId { get; init; } public static ServiceResult Ok(string message, Guid? productId = null) { return new ServiceResult { Success = true, Message = message, ProductId = productId }; } public static ServiceResult Fail(string message) { return new ServiceResult { Success = false, Message = message }; } }

运行命令

dotnet new console -n Week10Checkpoint cd Week10Checkpoint

把上面的代码放进 Program.cs,然后执行:

dotnet run

正确运行结果

True - 商品新增成功。 False - 商品名称不能为空。 True - 商品新增成功。 搜索 key:1 条 仅显示有库存:1 条 True - 商品编辑成功。 搜索 pro:1 条 True - 商品删除成功。 删除后总数:1 条

这份程序检查了什么

检查点对应代码合格标准
新增商品Create("Keyboard", "199", "10")返回成功
空名称校验Create("", "199", "10")返回失败
无库存商品Create("Monitor", "899", "0")能新增,用于过滤
搜索Search("key", false)返回 1 条
库存过滤Search("", true)只返回库存大于 0
编辑Update(...)返回成功,搜索新名称有效
删除Delete(...)返回成功,数量减少

Avalonia 项目验收清单

控制台程序通过后,还要检查 Avalonia 项目。

主列表页

1. 启动后能看到商品列表。 2. 点击商品后,选中项会变化。 3. 右侧详情或编辑区跟着变化。

编辑页

1. 编辑区使用 EditName、EditPriceText、EditStockText。 2. 修改输入框不会立刻污染原商品。 3. 点击保存后,列表同步更新。 4. 点击取消后,输入框恢复原数据。 5. 名称为空时不能保存。 6. 价格不是数字时不能保存。 7. 库存小于 0 时不能保存。

搜索过滤页

1. allProducts 保存全部数据。 2. VisibleProducts 显示当前结果。 3. 空关键字显示全部商品。 4. 搜索忽略大小写。 5. 仅显示有库存能隐藏库存为 0 的商品。 6. 没有结果时显示空状态。

最终通过标准

这一页通过时,应该能做到:

  1. 运行阶段检查程序,输出和上面一致。
  2. 能说清 Repository 和 Service 的区别。
  3. 能说清 SelectedProduct 和编辑草稿的区别。
  4. 能说清 allProductsVisibleProducts 的区别。
  5. 能在 Avalonia 页面里完成列表、编辑、搜索的主流程。

Week 10 过关的标准不是“页面看起来有东西”,而是需求、模型、规则、状态、绑定和验收能连成一条清楚的业务流程。