零售业务领域¶
本导读介绍演示所使用的零售分析数据模型、SQLite 配置、种子数据和 REST 端点,并明确指出为代码审查演示而刻意保留的代码异味。
领域模型¶
API 模型位于
AgentHQDemo.Api/Models。
Transaction
表示一次零售购买。它包含整数类型的 Id、 CustomerId、 Amount、
ProductCategory、StoreId、Timestamp 和 IsFlagged。该模型包含 Required、StringLength 和 Range 等数据注解;当控制器检查 ModelState 时,ASP.NET Core 模型绑定可以使用这些注解。
CustomerSegment
表示一个分析分群。它存储分群名称、描述、客户数量、月均消费额和留存率。
SegmentPrediction
是预测调用返回的不可变记录,其中包含客户 ID、预测分群、置信度分数和主要特征名称。
数据库与启动时播种¶
RetailDbContext
是一个小型 EF Core 上下文,包含两个数据集:
public DbSet<Transaction> Transactions => Set<Transaction>();
public DbSet<CustomerSegment> Segments => Set<CustomerSegment>();
Program.cs
使用 Data Source=retail.db配置 SQLite,注册
RetailAnalyticsService,并在应用启动期间播种数据:
RetailAnalyticsService.SeedDataAsync 对于正常的演示重启具有幂等性:只要已存在任何交易,它就会立即返回。对于全新数据库,它会插入示例交易和分群,然后保存更改。
种子数据¶
种子数据包含客户 C001 到 C005的 10 笔交易:
| 客户 | 种子数据模式 |
|---|---|
C001 |
杂货和电子产品购买,总额 335.49 |
C002 |
杂货和健康产品购买,总额 47.50 |
C003 |
电子产品和时尚产品购买,总额 1,700.00 |
C004 |
两笔低金额杂货购买,总额 21.49 |
C005 |
电子产品和时尚产品购买,总额 995.00 |
它还会创建四个客户分群:
| 分群 | 客户数量 | 月均消费额 | 留存率 | 描述 |
|---|---|---|---|---|
| 高价值 | 150 | $850 | 92% | 消费额排名前 10%,并表现出较强忠诚度的客户 |
| 常规 | 3,200 | $180 | 78% | 每月持续进行跨品类购物的客户 |
| 存在流失风险 | 890 | $95 | 45% | 过去 90 天内购买频率下降的客户 |
| 新客户 | 420 | $120 | 65% | 最近 90 天内加入的客户 |
REST 端点¶
TransactionsController
公开交易读取、创建和删除端点:
| 端点 | 返回内容 |
|---|---|
GET /api/transactions |
所有 Transaction 记录。 |
GET /api/transactions/{id} |
一个 Transaction;如果控制器收到 null,则返回 404。 |
POST /api/transactions |
创建一笔交易,并返回 201 Created 和已保存的记录。 |
DELETE /api/transactions/{id} |
204 No Content (删除成功时),或 404 (未找到时)。 |
SegmentsController
公开分群和预测端点:
| 端点 | 返回内容 |
|---|---|
GET /api/segments |
所有 CustomerSegment 记录。 |
GET /api/segments/{id} |
一个 CustomerSegment;未找到时返回 404。 |
GET /api/segments/predict/{customerId} |
该客户的一个 SegmentPrediction。 |
聊天端点在 通过 SSE 流式传输响应中介绍,因为它们属于 Copilot 流式传输路径,而不是零售数据 API。
客户分群预测逻辑¶
RetailAnalyticsService.PredictSegmentAsync
加载某位客户的所有交易,并计算总消费额、平均消费额和购买频率。随后按顺序应用以下规则:
- 没有交易:返回
New,置信度为0.5,特征为no_history。 - 总消费额高于
1000:返回High Value,置信度为0.89。 - 购买频率达到三次或以上:返回
Regular,置信度为0.75。 - 平均消费额低于
50:返回At Risk,置信度为0.62。 - 其他情况:返回
Regular,置信度为0.55。
返回的 TopFeatures 数组说明规则的输入,例如总消费额、购买频率或平均消费额。
四处刻意保留的代码异味¶
这些内容是专为代码审查环节设计的演示材料。在本演示中,请勿将其视为需要修复的意外 bug;应将其作为审查者需要发现并说明的问题示例。
1. RetailAnalyticsService.GetTransactionsWithSegmentsAsync 中的 N+1 查询¶
问题是什么:该方法加载所有交易,然后遍历每笔交易并调用 PredictSegmentAsync,从而针对该客户再次执行数据库查询。
为什么有问题:查询次数会随交易数量增长。对于少量种子数据尚可接受,但面对真实零售业务的数据规模时,可能变得缓慢且成本高昂。
审查者应指出:“这是 N+1 查询模式。请考虑批量处理客户交易数据,或基于该请求已加载的数据计算分群预测。”
2. RetailAnalyticsService.GetTransactionAsync 中缺少 null 检查¶
问题是什么:该服务使用 FindAsync(id) 并通过
!抑制可空性分析,返回 Task<Transaction> ,尽管数据库可能不会返回任何记录。
为什么有问题:调用方无法从签名判断 null 是否可能出现,未来的代码也可能在检查之前解引用结果。
审查者应指出:“服务契约应体现未找到的情况,例如返回 Transaction? 或结果类型,并由调用方显式处理。”
3. RetailAnalyticsService.AddTransactionAsync 中没有输入验证¶
问题是什么:该服务接收传入的 Transaction,设置
Timestamp = DateTime.UtcNow,然后在不检查金额、客户 ID、品类或门店值的情况下保存。
为什么有问题: TransactionsController.Create 会检查 ModelState,但测试、其他服务或未来的端点仍可直接调用该服务。无效的领域数据可能绕过控制器验证。
审查者应指出:“不要仅依赖控制器验证来保证领域不变量。请在服务中验证交易,或集中管理规则,确保每个调用方都获得相同的保护。”
4. RetailAnalyticsService.PredictSegmentAsync 中的硬编码阈值¶
问题是什么:高价值规则在代码中使用了字面量阈值 1000 。
为什么有问题:业务阈值会因市场、季节和零售商而变化。隐藏在代码中的魔法数字难以审计、调整,也难以向业务利益相关者解释。
审查者应指出:“请将高价值阈值移入配置或策略对象,使用清晰的名称,并通过测试覆盖边界行为。”