verify-task6.md 9.3 KB

Task 6 独立核验报告:指定商品会员价机制(BASIC 纳入)

核验方: 独立核验智能体
核验时间: 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

二、逐条核验

核验项 1: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 行将 BASIC mock 为 order=1(与 FREE 同级),
但生产 DB 经迁移 381(行 11822-11826)后 BASIC=2, FREE=1。
此测试 mocking 不准确,但不影响生产代码——生产代码直接查询 DB,行为正确。


核验项 2:所有调用点

调用点 文件:行号 方法 是否走 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 第 339/358 行:无条件下发 memberPrice

代码: CartService.java 行 338-358

dto.setMemberPrice(product.getMemberPrice());          // 行 339(常规 SKU)
dto.setMemberPrice(linkedProduct.getMemberPrice());    // 行 358(Linked SKU)

评估:

  • memberPrice 字段始终从 product 直接复制,不过滤用户等级
  • 同时 unitPrice 也设为 product.getPrice()(原价)
  • 注释(ProductOrderService.java 行 1741)明确:"会员体系重构:商品对所有用户统一定价,活动侧保留会员价,商品侧停用"——即前端自行判断是否展示会员价

结论: 这是一个已有行为,非本次改动引入;前端应通过 minDiscountLevel + 用户等级自行判断展示。
是否需要修: 建议由前端负责展示逻辑(当前设计如此)。后端不做过滤是正确的——避免端+后端双源判断不一致。


核验项 3: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) 获取等级后传入,逻辑完整。

结论:✅ 通过


核验项 4:限时特价/秒杀路径

代码位置: ProductOrderService.java 行 343-488(createSpecialDealOrder)

// 行 423-424
order.setUnitPrice(deal.getSpecialPrice());
order.setTotalAmount(deal.getSpecialPrice() * quantity);
  • 特价订单完全不调用 getEligibleMemberPrice
  • 原价 = 特价(deal.getSpecialPrice())
  • 该路径在本次任务变更前就已是如此(独立事务入口,规格 §5.2)

结论:✅ 通过,特价路径未被影响。


核验项 5:编译验证

mvn clean compile -q
# 退出码:0(无输出 = 无错误)

结论:✅ 通过


三、发现的问题

发现 1:测试 mock 不准确(不阻塞验收)

文件: 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 不一致(不影响生产行为)。