Day 3 - 服务层
建议用时:180-210 分钟
你将学会什么
- 服务层负责什么
- 为什么业务规则不写在界面里
- 什么是请求对象
- 什么是返回结果对象
- 怎么校验名称、价格、库存
- 怎么让服务层调用 Repository
数据层负责存取,服务层负责规则。今天重点是把“能不能新增、为什么不能新增、删除前有没有选中”这些判断放进 ProductService,不要散落在按钮事件或 ViewModel 里。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“数据层准备”,今天继续学习“服务层”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“服务层”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。服务层的核心问题是:业务规则到底放在哪里。
项目分层再看一遍
| 层 | 负责什么 | 不负责什么 |
|---|---|---|
| 界面层 | 显示、输入、按钮 | 不负责核心业务规则 |
| 服务层 | 校验、业务动作、结果返回 | 不负责控件排版 |
| 数据层 | 保存、读取、查询 | 不负责名称是否为空 |
例如“价格必须大于 0”不是界面规则,也不是文件规则,它是业务规则,所以放在服务层。
为什么不把规则写在按钮里
如果把规则写在按钮里,会出现这些问题:
- 新增页面写一遍校验。
- 编辑页面又写一遍校验。
- 批量导入也要写一遍校验。
- 以后规则改了,要到处找代码。
服务层集中规则后,界面只需要调用:
ServiceResult result = await service.CreateAsync(request);然后根据 result.Success 和 result.Message 显示结果。
什么是请求对象
请求对象是把一次操作需要的输入打包起来。
本页的新增请求是:
public sealed class CreateProductRequest
{
public string Name { get; set; } = "";
public string PriceText { get; set; } = "";
public string StockText { get; set; } = "";
}为什么价格和库存先用字符串?
因为界面输入框拿到的通常就是文本。服务层要判断:
- 文本能不能转成数字。
- 数字是否符合规则。
什么是返回结果对象
服务层可以抛异常,也可以返回结果对象。
本页对“名称为空、价格不是数字、库存小于 0”这类可预期问题,使用返回结果对象:
public sealed class ServiceResult
{
public bool Success { get; init; }
public string Message { get; init; } = "";
public Guid? ProductId { get; init; }
}好处是:
- 成功失败都能统一处理。
- UI 不需要用异常控制正常校验流程。
- 可以带回提示文字。
- 新增成功时还能带回商品 Id。
什么时候用异常
可预期的业务失败,用结果对象。
例如:
- 名称为空。
- 价格不是数字。
- 没有选中商品。
真正意外的问题,才适合异常。
例如:
- 文件无法写入。
- JSON 文件损坏。
- 磁盘权限失败。
这类问题可以在更外层捕获并写日志。
为什么用接口 IProductRepository
服务层不直接依赖具体存储。
本页用了:
public interface IProductRepository然后服务层依赖接口:
private readonly IProductRepository repository;这样好处是:
- 今天可以用
InMemoryProductRepository验证服务规则。 - 明天可以换成 JSON Repository。
- 后面也可以换成 SQLite Repository。
ProductService的业务规则不用改。
服务层方法名要表达业务动作
差的名字:
Do()
Save()
Handle()更清楚的名字:
CreateAsync()
DeleteAsync()
GetAllAsync()方法名清楚,后面 ViewModel 调用时就不容易乱。
服务层常用写法速查
| 需求 | 常用写法 | 说明 |
|---|---|---|
| 注入仓储 | ProductService(IProductRepository repo) | 服务不直接 new 数据层 |
| 请求对象 | CreateProductRequest | 保存输入参数 |
| 返回结果 | ServiceResult<T> | 成功/失败/错误 |
| 校验名称 | IsNullOrWhiteSpace | 必填 |
| 校验价格 | price <= 0 | 业务规则 |
| 查重 | Any(product => product.Name == name) | 防重复 |
| 成功返回 | ServiceResult.Ok(data) | 带结果 |
| 失败返回 | ServiceResult.Fail("原因") | 不抛给界面 |
服务层固定顺序:
接收请求
-> 校验输入
-> 查询必要数据
-> 执行业务规则
-> 调用仓储
-> 返回结果第二部分:把知识组合成完整例子
今天做一个能跑的 ProductService。
它会处理:
- 新增商品。
- 名称不能为空。
- 名称最多 50 个字符。
- 价格必须是数字,并且大于 0。
- 库存必须是整数,并且不能小于 0。
- 删除商品前必须传入商品 Id。
- 每次操作返回
ServiceResult,告诉调用者成功还是失败。
完整 Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
var repository = new InMemoryProductRepository();
var service = new ProductService(repository);
ServiceResult ok = await service.CreateAsync(new CreateProductRequest
{
Name = "Keyboard",
PriceText = "199",
StockText = "10"
});
Console.WriteLine($"{ok.Success} - {ok.Message}");
ServiceResult emptyName = await service.CreateAsync(new CreateProductRequest
{
Name = "",
PriceText = "199",
StockText = "10"
});
Console.WriteLine($"{emptyName.Success} - {emptyName.Message}");
ServiceResult badPrice = await service.CreateAsync(new CreateProductRequest
{
Name = "Mouse",
PriceText = "abc",
StockText = "10"
});
Console.WriteLine($"{badPrice.Success} - {badPrice.Message}");
ServiceResult badStock = await service.CreateAsync(new CreateProductRequest
{
Name = "Monitor",
PriceText = "899",
StockText = "-1"
});
Console.WriteLine($"{badStock.Success} - {badStock.Message}");
List<Product> products = await service.GetAllAsync();
Console.WriteLine($"当前商品数量:{products.Count}");
ServiceResult deleteWithoutId = await service.DeleteAsync(null);
Console.WriteLine($"{deleteWithoutId.Success} - {deleteWithoutId.Message}");
if (ok.ProductId is Guid productId)
{
ServiceResult deleted = await service.DeleteAsync(productId);
Console.WriteLine($"{deleted.Success} - {deleted.Message}");
}
List<Product> productsAfterDelete = await service.GetAllAsync();
Console.WriteLine($"删除后商品数量:{productsAfterDelete.Count}");
public sealed class CreateProductRequest
{
public string Name { get; set; } = "";
public string PriceText { get; set; } = "";
public string StockText { 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
};
}
}
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 interface IProductRepository
{
Task<List<Product>> GetAllAsync();
Task AddAsync(Product product);
Task<Product?> FindByIdAsync(Guid id);
Task DeleteAsync(Guid id);
}
public sealed class InMemoryProductRepository : IProductRepository
{
private readonly List<Product> products = new();
public Task<List<Product>> GetAllAsync()
{
return Task.FromResult(products.ToList());
}
public Task AddAsync(Product product)
{
products.Add(product);
return Task.CompletedTask;
}
public Task<Product?> FindByIdAsync(Guid id)
{
Product? product = products.FirstOrDefault(item => item.Id == id);
return Task.FromResult(product);
}
public Task DeleteAsync(Guid id)
{
products.RemoveAll(item => item.Id == id);
return Task.CompletedTask;
}
}
public sealed class ProductService
{
private readonly IProductRepository repository;
public ProductService(IProductRepository repository)
{
this.repository = repository;
}
public Task<List<Product>> GetAllAsync()
{
return repository.GetAllAsync();
}
public async Task<ServiceResult> CreateAsync(CreateProductRequest request)
{
string name = request.Name.Trim();
if (string.IsNullOrWhiteSpace(name))
{
return ServiceResult.Fail("商品名称不能为空。");
}
if (name.Length > 50)
{
return ServiceResult.Fail("商品名称不能超过 50 个字符。");
}
if (!decimal.TryParse(request.PriceText, out decimal price))
{
return ServiceResult.Fail("价格必须是数字。");
}
if (price <= 0)
{
return ServiceResult.Fail("价格必须大于 0。");
}
if (!int.TryParse(request.StockText, out int stock))
{
return ServiceResult.Fail("库存必须是整数。");
}
if (stock < 0)
{
return ServiceResult.Fail("库存不能小于 0。");
}
DateTime now = DateTime.Now;
var product = new Product
{
Id = Guid.NewGuid(),
Name = name,
Price = price,
Stock = stock,
CreatedAt = now,
UpdatedAt = now
};
await repository.AddAsync(product);
return ServiceResult.Ok("商品新增成功。", product.Id);
}
public async Task<ServiceResult> DeleteAsync(Guid? productId)
{
if (productId is null)
{
return ServiceResult.Fail("请先选择要删除的商品。");
}
Product? product = await repository.FindByIdAsync(productId.Value);
if (product is null)
{
return ServiceResult.Fail("要删除的商品不存在。");
}
await repository.DeleteAsync(productId.Value);
return ServiceResult.Ok("商品删除成功。");
}
}正常运行结果
True - 商品新增成功。
False - 商品名称不能为空。
False - 价格必须是数字。
False - 库存不能小于 0。
当前商品数量:1
False - 请先选择要删除的商品。
True - 商品删除成功。
删除后商品数量:0这个例子展示了服务层的价值:它不是简单转发 Repository,而是集中处理业务规则,并把结果返回给调用者。
第三部分:跟着敲代码
从这里开始动手。今天仍然先用控制台项目验证服务层。
第 1 步:创建控制台项目
dotnet new console -n ProductServiceDemo
cd ProductServiceDemo打开 Program.cs,清空原内容。
第 2 步:写请求对象
public sealed class CreateProductRequest
{
public string Name { get; set; } = "";
public string PriceText { get; set; } = "";
public string StockText { get; set; } = "";
}解释:
Name来自商品名输入。PriceText来自价格输入框。StockText来自库存输入框。
第 3 步:写返回结果对象
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
};
}
}解释:
Success表示成功还是失败。Message是给界面显示的提示。ProductId用来在新增成功时带回 Id。
第 4 步:写 Product
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; }
}解释:
- 这和 Day 2 的数据模型一致。
第 5 步:写 Repository 接口
public interface IProductRepository
{
Task<List<Product>> GetAllAsync();
Task AddAsync(Product product);
Task<Product?> FindByIdAsync(Guid id);
Task DeleteAsync(Guid id);
}解释:
- 服务层只需要这些数据动作。
- 具体怎么保存,接口不关心。
第 6 步:写内存 Repository
public sealed class InMemoryProductRepository : IProductRepository
{
private readonly List<Product> products = new();
public Task<List<Product>> GetAllAsync()
{
return Task.FromResult(products.ToList());
}
public Task AddAsync(Product product)
{
products.Add(product);
return Task.CompletedTask;
}
public Task<Product?> FindByIdAsync(Guid id)
{
Product? product = products.FirstOrDefault(item => item.Id == id);
return Task.FromResult(product);
}
public Task DeleteAsync(Guid id)
{
products.RemoveAll(item => item.Id == id);
return Task.CompletedTask;
}
}解释:
- 这里用内存列表,是为了专心验证服务层。
- 后面可以换成 Day 2 的 JSON Repository。
第 7 步:写 ProductService 构造函数
public sealed class ProductService
{
private readonly IProductRepository repository;
public ProductService(IProductRepository repository)
{
this.repository = repository;
}
}解释:
- 通过构造函数把 Repository 传进来。
- 这叫依赖注入的基础形式。
第 8 步:写 GetAllAsync
public Task<List<Product>> GetAllAsync()
{
return repository.GetAllAsync();
}解释:
- 获取全部商品暂时不需要业务规则。
- 所以直接调用 Repository。
第 9 步:写新增校验
public async Task<ServiceResult> CreateAsync(CreateProductRequest request)
{
string name = request.Name.Trim();
if (string.IsNullOrWhiteSpace(name))
{
return ServiceResult.Fail("商品名称不能为空。");
}
if (name.Length > 50)
{
return ServiceResult.Fail("商品名称不能超过 50 个字符。");
}
if (!decimal.TryParse(request.PriceText, out decimal price))
{
return ServiceResult.Fail("价格必须是数字。");
}
if (price <= 0)
{
return ServiceResult.Fail("价格必须大于 0。");
}
if (!int.TryParse(request.StockText, out int stock))
{
return ServiceResult.Fail("库存必须是整数。");
}
if (stock < 0)
{
return ServiceResult.Fail("库存不能小于 0。");
}解释:
- 先处理名称。
- 再处理价格。
- 最后处理库存。
- 每个失败都返回明确原因。
第 10 步:新增成功时保存商品
继续接在 CreateAsync 里:
DateTime now = DateTime.Now;
var product = new Product
{
Id = Guid.NewGuid(),
Name = name,
Price = price,
Stock = stock,
CreatedAt = now,
UpdatedAt = now
};
await repository.AddAsync(product);
return ServiceResult.Ok("商品新增成功。", product.Id);
}解释:
- 校验全部通过,才创建商品。
- 新增成功后返回成功结果和商品 Id。
第 11 步:写删除规则
public async Task<ServiceResult> DeleteAsync(Guid? productId)
{
if (productId is null)
{
return ServiceResult.Fail("请先选择要删除的商品。");
}
Product? product = await repository.FindByIdAsync(productId.Value);
if (product is null)
{
return ServiceResult.Fail("要删除的商品不存在。");
}
await repository.DeleteAsync(productId.Value);
return ServiceResult.Ok("商品删除成功。");
}解释:
- 没选中商品不能删除。
- 商品不存在也不能删除。
- 删除成功后返回成功消息。
第 12 步:写验证代码
var repository = new InMemoryProductRepository();
var service = new ProductService(repository);
ServiceResult ok = await service.CreateAsync(new CreateProductRequest
{
Name = "Keyboard",
PriceText = "199",
StockText = "10"
});
Console.WriteLine($"{ok.Success} - {ok.Message}");再依次验证空名称、错误价格、错误库存、删除未选择、正常删除。
常见错误和修法
| 错误 | 为什么错 | 修法 |
|---|---|---|
| 服务层只转发 Repository | 没有承担业务规则 | 把名称、价格、库存校验放进服务层 |
| 校验失败还继续保存 | 错误数据会进入文件 | 返回失败结果或直接停止保存流程 |
| ViewModel 直接操作文件 | 界面和数据保存耦合 | ViewModel 调用 Service,Service 调用 Repository |
错误结果只有 false | 不知道失败原因 | 返回 Success、Message、Errors |
| 方法名太模糊 | 调用时看不出意图 | 用 CreateProductAsync、UpdateProductAsync 这类动词 |
小白重复敲写训练
服务层每次只增加一条业务规则。
训练 1:名称清洗后保存
public void Create(string name, decimal price)
{
string cleanName = name.Trim();
var product = new Product { Name = cleanName, Price = price };
_repository.Add(product);
}训练 2:拒绝空名称
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("商品名称不能为空", nameof(name));
}分别传入正常名称和空格。
训练 3:拒绝重复名称
bool exists = _repository.GetAll()
.Any(product => product.Name.Equals(cleanName, StringComparison.OrdinalIgnoreCase));
if (exists)
{
throw new InvalidOperationException("商品名称已存在");
}第三遍为这条规则写成功和失败两个测试。
每日小测
做完本页后,用这 5 题检查是否真的掌握。
1. 判断题
本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。
答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。
2. 填空题
本页主题是:服务层。今天至少要掌握的 3 个点是:
1. 服务层负责什么
2. 为什么业务规则不写在界面里
3. 什么是请求对象答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。
3. 流程题
遇到本页相关功能时,先按什么顺序处理?
答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。
4. 找错误题
如果本页代码运行失败,第一步应该做什么?
答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。
5. 改需求题
在本页完整例子跑通后,至少改一个小需求。
可选改法:
- 改一个字段名称。
- 多加一个校验条件。
- 多输出一行结果。
- 把固定数据改成用户输入。
- 把一次处理改成多条数据处理。
答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:服务层负责什么。
上位机专项练习
轮询服务定时读取设备,把结果交给缓存、报警和界面,不能让 ViewModel 直接处理底层通信。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:轮询一台设备
public async Task PollAsync(Device device, CancellationToken token)
{
Reading reading = await reader.ReadAsync(device, token);
cache.Update(reading);
alarmService.Check(reading);
}运行结果或界面效果:
读取、缓存、报警职责依次执行改动任务: 增加读取失败日志。
专项例子 2:持续轮询循环
while (!token.IsCancellationRequested)
{
await PollAllDevicesAsync(token);
await Task.Delay(options.PollInterval, token);
}运行结果或界面效果:
按配置周期重复采集改动任务: 测量每轮耗时,避免间隔越来越不准。
专项例子 3:启动和停止服务
public Task StartAsync()
{
cts = new CancellationTokenSource();
pollingTask = RunAsync(cts.Token);
return Task.CompletedTask;
}
public async Task StopAsync()
{
cts?.Cancel();
if (pollingTask is not null) await pollingTask;
}运行结果或界面效果:
服务可以安全启动和停止改动任务: 防止重复点击启动。
第四部分:作业完整答案
作业要求:写一个 ProductService,用请求对象接收输入,用返回结果对象表示成功失败,并完成新增和删除的业务规则。
答案文件:Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
var repository = new InMemoryProductRepository();
var service = new ProductService(repository);
ServiceResult ok = await service.CreateAsync(new CreateProductRequest
{
Name = "Keyboard",
PriceText = "199",
StockText = "10"
});
Console.WriteLine($"{ok.Success} - {ok.Message}");
ServiceResult emptyName = await service.CreateAsync(new CreateProductRequest
{
Name = "",
PriceText = "199",
StockText = "10"
});
Console.WriteLine($"{emptyName.Success} - {emptyName.Message}");
ServiceResult badPrice = await service.CreateAsync(new CreateProductRequest
{
Name = "Mouse",
PriceText = "abc",
StockText = "10"
});
Console.WriteLine($"{badPrice.Success} - {badPrice.Message}");
ServiceResult badStock = await service.CreateAsync(new CreateProductRequest
{
Name = "Monitor",
PriceText = "899",
StockText = "-1"
});
Console.WriteLine($"{badStock.Success} - {badStock.Message}");
List<Product> products = await service.GetAllAsync();
Console.WriteLine($"当前商品数量:{products.Count}");
ServiceResult deleteWithoutId = await service.DeleteAsync(null);
Console.WriteLine($"{deleteWithoutId.Success} - {deleteWithoutId.Message}");
if (ok.ProductId is Guid productId)
{
ServiceResult deleted = await service.DeleteAsync(productId);
Console.WriteLine($"{deleted.Success} - {deleted.Message}");
}
List<Product> productsAfterDelete = await service.GetAllAsync();
Console.WriteLine($"删除后商品数量:{productsAfterDelete.Count}");
public sealed class CreateProductRequest
{
public string Name { get; set; } = "";
public string PriceText { get; set; } = "";
public string StockText { 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
};
}
}
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 interface IProductRepository
{
Task<List<Product>> GetAllAsync();
Task AddAsync(Product product);
Task<Product?> FindByIdAsync(Guid id);
Task DeleteAsync(Guid id);
}
public sealed class InMemoryProductRepository : IProductRepository
{
private readonly List<Product> products = new();
public Task<List<Product>> GetAllAsync()
{
return Task.FromResult(products.ToList());
}
public Task AddAsync(Product product)
{
products.Add(product);
return Task.CompletedTask;
}
public Task<Product?> FindByIdAsync(Guid id)
{
Product? product = products.FirstOrDefault(item => item.Id == id);
return Task.FromResult(product);
}
public Task DeleteAsync(Guid id)
{
products.RemoveAll(item => item.Id == id);
return Task.CompletedTask;
}
}
public sealed class ProductService
{
private readonly IProductRepository repository;
public ProductService(IProductRepository repository)
{
this.repository = repository;
}
public Task<List<Product>> GetAllAsync()
{
return repository.GetAllAsync();
}
public async Task<ServiceResult> CreateAsync(CreateProductRequest request)
{
string name = request.Name.Trim();
if (string.IsNullOrWhiteSpace(name))
{
return ServiceResult.Fail("商品名称不能为空。");
}
if (name.Length > 50)
{
return ServiceResult.Fail("商品名称不能超过 50 个字符。");
}
if (!decimal.TryParse(request.PriceText, out decimal price))
{
return ServiceResult.Fail("价格必须是数字。");
}
if (price <= 0)
{
return ServiceResult.Fail("价格必须大于 0。");
}
if (!int.TryParse(request.StockText, out int stock))
{
return ServiceResult.Fail("库存必须是整数。");
}
if (stock < 0)
{
return ServiceResult.Fail("库存不能小于 0。");
}
DateTime now = DateTime.Now;
var product = new Product
{
Id = Guid.NewGuid(),
Name = name,
Price = price,
Stock = stock,
CreatedAt = now,
UpdatedAt = now
};
await repository.AddAsync(product);
return ServiceResult.Ok("商品新增成功。", product.Id);
}
public async Task<ServiceResult> DeleteAsync(Guid? productId)
{
if (productId is null)
{
return ServiceResult.Fail("请先选择要删除的商品。");
}
Product? product = await repository.FindByIdAsync(productId.Value);
if (product is null)
{
return ServiceResult.Fail("要删除的商品不存在。");
}
await repository.DeleteAsync(productId.Value);
return ServiceResult.Ok("商品删除成功。");
}
}验收结果
运行后必须看到:
- 合法商品新增成功。
- 空名称返回失败。
- 错误价格返回失败。
- 小于 0 的库存返回失败。
- 新增成功后商品数量是 1。
- 未选择商品时删除失败。
- 删除已存在商品成功。
- 删除后商品数量是 0。
为什么这个答案是对的
这个答案把服务层职责拆清楚了:
| 职责 | 代码 |
|---|---|
| 接收输入 | CreateProductRequest |
| 返回结果 | ServiceResult |
| 校验规则 | CreateAsync |
| 调用数据层 | repository.AddAsync |
| 删除规则 | DeleteAsync |
| 隔离存储实现 | IProductRepository |
服务层不是多余的一层。它让规则集中,后面主列表、详情编辑、搜索页面都能复用同一套业务判断。