Просмотр исходного кода

S-cfc-177: supplier.order.fulfill — ProductOrderService#redeem(SC-S-04) 契约落地

Sisyphus Agent 2 часов назад
Родитель
Сommit
808b603f29

+ 86 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java

@@ -1695,6 +1695,92 @@ public class ProductOrderService {
         return Result.success("已确认发出");
     }
 
+    /**
+     * 服务商订单到店核销(SC-S-04,能力键 {@code supplier.order.fulfill})。
+     * 契约签名:{@code public Result<String> redeem(String orderNo, String pickupCode, Long vendorId)}。
+     *
+     * <p>行为矩阵(见 {@code SupplierOrderFulfillContractTest} / {@code 应用场景总览.md} SC-S-04):
+     * <ul>
+     *   <li>正常流:本服务商 + 自提码匹配 + 未超期 → 订单置「已完成」,返回成功;</li>
+     *   <li>防冒用(TC-S-03-E):自提码不匹配 → 拒绝(提示核销码校验失败),不落库;</li>
+     *   <li>重复核销幂等(TC-S-04):已 completed 再次核销 → 返回成功,不重复落库/副作用;</li>
+     *   <li>超期订单(TC-S-04):自提时段已过 → 提示不可核销,状态不变更;</li>
+     *   <li>非本服务商(TC-S-04-P):商品 vendorId ≠ vendorId → 无权操作,不落库。</li>
+     * </ul>
+     */
+    public Result<String> redeem(String orderNo, String pickupCode, Long vendorId) {
+        ProductOrder order = orderMapper.selectOne(
+            new LambdaQueryWrapper<ProductOrder>().eq(ProductOrder::getOrderNo, orderNo));
+        if (order == null) {
+            return Result.error("订单不存在");
+        }
+        // 幂等:已核销完成(重复扫码同订单)→ 返回成功,不重复落库/不重复副作用
+        if ("completed".equals(order.getStatus())) {
+            return Result.success("该订单已完成核销");
+        }
+        if (!"paid".equals(order.getStatus())) {
+            return Result.error("当前订单状态不可核销");
+        }
+        // 超期判定:自提时段快照已过 → 不可核销(状态不变更)
+        if (isPickupExpired(order)) {
+            return Result.error("订单已超期,不可核销");
+        }
+        // 权限:非本服务商商品订单 → 无权操作(不落库)
+        Product product = order.getProductId() != null ? productMapper.selectById(order.getProductId()) : null;
+        if (product == null || vendorId == null || !vendorId.equals(product.getVendorId())) {
+            return Result.error("无权操作此订单");
+        }
+        // 防冒用:自提码校验(TC-S-03-E)
+        if (pickupCode == null || order.getPickupCode() == null || !pickupCode.equals(order.getPickupCode())) {
+            return Result.error("核销码校验失败");
+        }
+        // 核销完成:到店核销无实体物流,置终态 + 履约扩展标记
+        order.setStatus("completed");
+        order.setFulfillStatus("received_by_user");
+        order.setReceivedAt(new Date());
+        order.setUpdatedAt(new Date());
+        orderMapper.updateById(order);
+        return Result.success("核销成功");
+    }
+
+    /**
+     * 超期判定:自提时段快照 {@code timeSlotSnapshot} 中 {@code endTime} 已过(早于当前时刻)。
+     * 快照 {@code endTime} 可能为 {@code java.sql.Time} 时间串(如 "09:00:00")或完整日期串;
+     * 仅当能解析为完整日期且早于 now 时判定超期——纯时间串无法定位具体日期,不在此简单判定(避免误伤当日时段)。
+     */
+    private boolean isPickupExpired(ProductOrder order) {
+        String snap = order.getTimeSlotSnapshot();
+        if (snap == null || snap.trim().isEmpty()) {
+            return false;
+        }
+        try {
+            com.alibaba.fastjson.JSONObject obj = JSON.parseObject(snap);
+            String endTime = obj.getString("endTime");
+            if (endTime == null || endTime.trim().isEmpty()) {
+                return false;
+            }
+            Date end = parseSlotEndTime(endTime.trim());
+            return end != null && end.before(new Date());
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    /** 解析快照 endTime:支持完整日期串 "yyyy-MM-dd HH:mm:ss" / "yyyy-MM-dd'T'HH:mm:ss";纯时间串返回 null */
+    private Date parseSlotEndTime(String s) {
+        String[] patterns = {"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss"};
+        for (String p : patterns) {
+            try {
+                SimpleDateFormat sdf = new SimpleDateFormat(p);
+                sdf.setLenient(false);
+                return sdf.parse(s);
+            } catch (Exception ignored) {
+                // 尝试下一个模式
+            }
+        }
+        return null;
+    }
+
     // ========== 退款流程 ==========
 
     public Result<String> applyRefund(String orderNo, Long userId, String reason) {