跳转到正文

零售业务领域

本导读介绍演示所使用的零售分析数据模型、SQLite 配置、种子数据和 REST 端点,并明确指出为代码审查演示而刻意保留的代码异味。

领域模型

API 模型位于 AgentHQDemo.Api/Models

Transaction 表示一次零售购买。它包含整数类型的 IdCustomerIdAmountProductCategoryStoreIdTimestampIsFlagged。该模型包含 RequiredStringLengthRange 等数据注解;当控制器检查 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,并在应用启动期间播种数据:

await db.Database.EnsureCreatedAsync();
await service.SeedDataAsync();

RetailAnalyticsService.SeedDataAsync 对于正常的演示重启具有幂等性:只要已存在任何交易,它就会立即返回。对于全新数据库,它会插入示例交易和分群,然后保存更改。

种子数据

种子数据包含客户 C001C005的 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 加载某位客户的所有交易,并计算总消费额、平均消费额和购买频率。随后按顺序应用以下规则:

  1. 没有交易:返回 New ,置信度为 0.5 ,特征为 no_history
  2. 总消费额高于 1000:返回 High Value ,置信度为 0.89
  3. 购买频率达到三次或以上:返回 Regular ,置信度为 0.75
  4. 平均消费额低于 50:返回 At Risk ,置信度为 0.62
  5. 其他情况:返回 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

为什么有问题:业务阈值会因市场、季节和零售商而变化。隐藏在代码中的魔法数字难以审计、调整,也难以向业务利益相关者解释。

审查者应指出:“请将高价值阈值移入配置或策略对象,使用清晰的名称,并通过测试覆盖边界行为。”