核验方: 独立核验智能体
核验时间: 2026-10-24
事实源: 代码实际行为(不信任 exec-task6.md 结论)
| # | 核验项 | 说明 |
|---|---|---|
| 1 | getEligibleMemberPrice 逻辑 |
min_discount_level=NULL 时按 BASIC 判定;等级比较方向正确;FREE/PROVIDER 不误享 |
| 2 | 调用点全覆盖 | 单商品下单、多商品下单;确认无绕过路径;评估 CartService/ProductDTO 行为 |
| 3 | ActivityDTO.resolveMemberPrice |
BASIC 纳入;FREE/PROVIDER 不享 |
| 4 | 限时特价/秒杀路径 | 不受本次改动影响 |
| 5 | 编译验证 | mvn clean compile -q 退出码=0 |
ProductOrderService.getEligibleMemberPrice代码位置: ProductOrderService.java 第 2166–2188 行
private Integer getEligibleMemberPrice(Product product, Long buyerId) {
if (product == null || product.getMemberPrice() == null || product.getMemberPrice() <= 0) {
return product != null ? product.getPrice() : 0;
}
// 用户会员等级(FREE 兜底)
String userLevel = "FREE";
if (buyerId != null) {
User user = userMapper.selectById(buyerId);
if (user != null) {
userLevel = membershipService.getMemberLevel(buyerId); // ← 行 2175
}
}
// 产品未设置最低级别时默认 BASIC
String minLevel = (product.getMinDiscountLevel() == null || product.getMinDiscountLevel().isEmpty())
? "BASIC" : product.getMinDiscountLevel(); // ← 行 2179-2180
// 比较级别数字:级别数字越小越低
int userLevelOrder = membershipService.getLevelOrder(userLevel); // ← 行 2182
int minLevelOrder = membershipService.getLevelOrder(minLevel); // ← 行 2183
// 用户级别 >= 最低要求级别,则可享会员价
if (userLevelOrder >= minLevelOrder) { // ← 行 2185
return product.getMemberPrice();
}
return product.getPrice();
}
membershipService.getMemberLevel 行为(MembershipService.java 行 1261–1283):
buyerId 为 null → 返回 "FREE"user.getMemberLevel() == "PROVIDER" → 强制转为 "FREE"(行 1270-1271)resolveEffectiveLevel,行 1309-1323)production getLevelOrder 值(DatabaseInitializer 迁移 381,行 11820-11831):
| LevelCode | level_order |
|---|---|
| FREE | 1 |
| BASIC | 2 |
| FAMILY | 3 |
| PREMIUM | 4 |
| LIFETIME | 5 |
| PROVIDER | 1(与 FREE 同级,视为非会员) |
场景矩阵:
| 用户等级 | min_discount_level | userLevelOrder | minLevelOrder | 结果 | 预期 |
|---|---|---|---|---|---|
| FREE | NULL/空 | 1 | 2 | 原价 | ✅ |
| FREE | "BASIC" | 1 | 2 | 原价 | ✅ |
| FREE | "FAMILY" | 1 | 3 | 原价 | ✅ |
| BASIC | NULL/空 | 2 | 2 | 会员价 | ✅ |
| BASIC | "BASIC" | 2 | 2 | 会员价 | ✅ |
| BASIC | "FAMILY" | 2 | 3 | 原价 | ✅ |
| FAMILY | NULL/空 | 3 | 2 | 会员价 | ✅ |
| PREMIUM | NULL/空 | 4 | 2 | 会员价 | ✅ |
| PROVIDER | NULL/空 | 1 | 2 | 原价 | ✅ |
| null buyerId | 任何 | 1(兜底) | 2 | 原价 | ✅ |
结论:✅ 通过
附注:测试数据与实际数据库不一致
MembershipServiceTest.java第 67 行将BASICmock 为 order=1(与 FREE 同级),
但生产 DB 经迁移 381(行 11822-11826)后BASIC=2, FREE=1。
此测试 mocking 不准确,但不影响生产代码——生产代码直接查询 DB,行为正确。
| 调用点 | 文件:行号 | 方法 | 是否走 getEligibleMemberPrice |
|---|---|---|---|
| 单商品下单 | ProductOrderService.java:263 |
create() |
✅ order.setUnitPrice(getEligibleMemberPrice(product, buyerId)) |
| 单商品下单金额 | ProductOrderService.java:264 |
create() |
✅ order.setTotalAmount(getEligibleMemberPrice(product, buyerId) * quantity) |
| 多商品下单(购物车) | ProductOrderService.java:762 |
createMultiItem() |
✅ int unitPrice = getEligibleMemberPrice(product, buyerId) |
| 限时特价(秒杀) | ProductOrderService.java:423 |
createSpecialDealOrder() |
❌ 使用 deal.getSpecialPrice(),不经过本方法(设计如此) |
无绕过路径。 所有结算价路径(单商品/多商品)均经 getEligibleMemberPrice。
代码: CartService.java 行 338-358
dto.setMemberPrice(product.getMemberPrice()); // 行 339(常规 SKU)
dto.setMemberPrice(linkedProduct.getMemberPrice()); // 行 358(Linked SKU)
评估:
memberPrice 字段始终从 product 直接复制,不过滤用户等级unitPrice 也设为 product.getPrice()(原价)结论: 这是一个已有行为,非本次改动引入;前端应通过 minDiscountLevel + 用户等级自行判断展示。
是否需要修: 建议由前端负责展示逻辑(当前设计如此)。后端不做过滤是正确的——避免端+后端双源判断不一致。
ActivityDTO.resolveMemberPrice代码位置: ActivityDTO.java 第 137-146 行
public static Integer resolveMemberPrice(Integer basePrice, Integer memberPrice, String memberLevel) {
if (basePrice == null) return null;
// If user has member level and a memberPrice is set, use memberPrice
if (memberLevel != null && ("BASIC".equals(memberLevel) || "FAMILY".equals(memberLevel) || "PREMIUM".equals(memberLevel))) {
if (memberPrice != null) {
return memberPrice;
}
}
return basePrice;
}
场景矩阵:
| memberLevel | 结果 | 预期 |
|---|---|---|
| null | basePrice | ✅ 非会员 |
| "FREE" | basePrice | ✅ 免费用户不享 |
| "BASIC" | memberPrice(若存在) | ✅ BASIC 纳入 |
| "FAMILY" | memberPrice(若存在) | ✅ 保持不变 |
| "PREMIUM" | memberPrice(若存在) | ✅ 保持不变 |
| "PROVIDER" | basePrice | ✅ 服务商不享 |
调用方: ActivityService.java:117, 140 均通过 membershipService.getMemberLevel(userId) 获取等级后传入,逻辑完整。
结论:✅ 通过
代码位置: ProductOrderService.java 行 343-488(createSpecialDealOrder)
// 行 423-424
order.setUnitPrice(deal.getSpecialPrice());
order.setTotalAmount(deal.getSpecialPrice() * quantity);
getEligibleMemberPricedeal.getSpecialPrice())结论:✅ 通过,特价路径未被影响。
mvn clean compile -q
# 退出码:0(无输出 = 无错误)
结论:✅ 通过
文件: MembershipServiceTest.java 行 66-67
doReturn(1).when(service).getLevelOrder("FREE"); // 测试中 FREE=1
doReturn(1).when(service).getLevelOrder("BASIC"); // 测试中 BASIC=1 ← 与生产不符
问题: 测试将 BASIC mock 为 level_order=1(与 FREE 同级),导致 getEligibleMemberPrice 在测试环境中 FREE 和 BASIC 行为相同。
实际生产值: 迁移 381(行 11820-11831)确保 BASIC=2, FREE=1。
影响: 单元测试无法验证"FREE 不享会员价"的逻辑分支(在 mock 环境下不区分)。
是否修复: 本次核验不修改测试代码(测试 mock 是历史遗留,与需求实现无关)。建议后续由开发方修正测试数据。
| 核验项 | 结论 |
|---|---|
| 1. getEligibleMemberPrice 逻辑正确性 | ✅ 通过 |
| 2. ActivityDTO.resolveMemberPrice 纳入 BASIC | ✅ 通过 |
| 3. 单商品/多商品调用点一致,无绕过 | ✅ 通过 |
| 4. 限时特价/秒杀路径不受影响 | ✅ 通过 |
| 5. mvn clean compile 编译通过 | ✅ 通过 |
是否建议验收: ✅ 建议验收。代码实现符合需求口径。BASIC 用户购买 member_price 设置且 min_discount_level 为 NULL/BASIC 的商品时获得会员价;FREE 用户获原价;FAMILY/PREMIUM 继续获会员价;min_discount_level=FAMILY 的商品 BASIC 用户获原价。唯一发现是单元测试 mock 数据与生产 DB 不一致(不影响生产行为)。