Week 10 - 阶段检查
这一页用来确认 Week 10 是否真正掌握:能从需求出发,写出数据层、服务层、主列表、编辑和搜索过滤的完整业务流程。
本周学习地图
| 阶段 | 先掌握什么 | 最终能写什么 |
|---|---|---|
| 需求 | 范围、字段、规则、验收 | 能把需求写成可实现清单 |
| 数据层 | 模型、Repository、JSON | 能保存和读取商品 |
| 服务层 | 校验、业务结果、错误消息 | 能集中处理规则 |
| 主列表 | 加载、选择、空状态 | 能显示多条商品 |
| 编辑 | 草稿、保存、取消、删除 | 能安全修改商品 |
| 搜索 | 全部数据和可见数据 | 能组合过滤条件 |
本周自测项目:商品管理 MVP
从空 Avalonia 项目开始做最小商品管理系统,至少包含:
Product模型。ProductRepository保存 JSON。ProductService校验名称、价格、库存。- 商品列表、新增、编辑、删除。
- 关键词搜索和分类过滤。
- 错误提示、空状态、保存成功提示。
过关标准:能解释 View、ViewModel、Service、Repository 每层放什么。
上位机阶段验收:上位机主项目第一版
本周目标: 打通需求、模型、轮询、报警、仪表盘、详情和搜索筛选。
必须亲手完成:
- 模拟 3 台设备持续产生点位数据
- 首页摘要和详情实时更新
- 制造越限值后生成并显示报警
过关标准: 从启动采集到查看详情和报警形成完整闭环,单台失败不影响其他设备。
不要只看答案。新建一个空项目重做一次,运行成功后再故意改坏一处并自己排错。
本周流程图
需求文档
-> Product 模型
-> Repository 读写 JSON
-> Service 校验名称、价格、库存
-> ViewModel 保存输入、列表、选中项
-> View 绑定输入框、按钮、列表
-> 用户完成新增、编辑、删除、搜索前半部分:本周原理地图
Week 10 的主线是:
把一个商品管理需求,拆成数据、规则和界面三层,再做出能新增、编辑、搜索、删除的最小业务系统。这一周不是只练控件。真正要掌握的是:需求怎么落到字段,字段怎么落到模型,规则怎么放到服务层,界面怎么绑定状态。
能力地图
| 能力 | 解决的问题 | 最小判断 |
|---|---|---|
| 需求范围 | 防止功能失控 | 能写清做什么和不做什么 |
| 数据模型 | 定义商品长什么样 | 能写出 Product 字段 |
| 数据层 | 封装保存和读取 | 能写 Repository |
| 服务层 | 集中业务规则 | 能校验名称、价格、库存 |
| 主列表 | 显示和选择多条数据 | 能绑定 ObservableCollection |
| 详情编辑 | 修改当前选中商品 | 能用编辑草稿保存和取消 |
| 搜索过滤 | 从全部数据筛出可见数据 | 能区分全部数据和可见数据 |
分层判断
| 层 | 应该放什么 | 不应该放什么 |
|---|---|---|
| View / XAML | 控件、布局、绑定 | 不写业务规则 |
| ViewModel | 页面状态、命令、消息 | 不直接处理文件细节 |
| Service | 业务校验、业务动作 | 不写控件布局 |
| Repository | 保存、读取、查询 | 不判断价格是否大于 0 |
| Model | 数据字段 | 不处理按钮点击 |
如果一个价格校验写在 XAML 里,位置就错了。
如果一个 JSON 文件路径写在按钮事件里,位置也错了。
本周必须掌握的 7 个点
- 需求文档要写“做什么”和“不做什么”。
Product要有Id、Name、Price、Stock、CreatedAt、UpdatedAt。- Repository 负责保存和读取。
- Service 负责校验和业务动作。
SelectedProduct表示当前选中的一条数据。- 编辑表单要用草稿字段,不要直接污染原对象。
- 搜索过滤要保留全部数据,只刷新可见结果。
Week 10 必须会用的操作
| 主题 | 必须会的写法 |
|---|---|
| 需求 | MVP 范围、字段、业务规则、验收标准 |
| Model | Product、Id、Name、Price、Stock |
| Repository | GetAll、FindById、Add、Update、Delete |
| JSON | JsonSerializer.Serialize、Deserialize、File.ReadAllText、WriteAllText |
| Service | CreateProductRequest、ServiceResult、校验和查重 |
| ViewModel 列表 | ObservableCollection<ProductItem> |
| 选中项 | SelectedProduct |
| 编辑草稿 | EditName、EditPriceText、EditStockText |
| 搜索 | Where、Contains、ToLower |
| 过滤 | OnlyInStock、VisibleProducts |
下半部分:阶段检查完整答案
下面是一份完整阶段检查程序。它会验证 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. 没有结果时显示空状态。最终通过标准
这一页通过时,应该能做到:
- 运行阶段检查程序,输出和上面一致。
- 能说清 Repository 和 Service 的区别。
- 能说清
SelectedProduct和编辑草稿的区别。 - 能说清
allProducts和VisibleProducts的区别。 - 能在 Avalonia 页面里完成列表、编辑、搜索的主流程。
Week 10 过关的标准不是“页面看起来有东西”,而是需求、模型、规则、状态、绑定和验收能连成一条清楚的业务流程。