diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCarVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCarVo.java
index d1e462e3..0ba972bd 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCarVo.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCarVo.java
@@ -24,6 +24,26 @@ public class HardCarVo extends HardCarEntity {
/** 商品购买次数汇总值,仅统计查询赋值。 */
private Long payCount;
+ /**
+ * 原订单购买份数,只读展示字段;普通订单商品列表中与持久化 {@link #getProductCount()} 一致。
+ */
+ private Integer originalProductCount;
+
+ /**
+ * 已完成退款份数,只统计售后状态为完成的商品明细;空响应按零处理。
+ */
+ private Integer refundedProductCount;
+
+ /**
+ * 申请中退款份数,只统计仍占用退款额度的待处理售后;不从当前可履约数量中提前扣除。
+ */
+ private Integer applyingProductCount;
+
+ /**
+ * 当前剩余可履约份数,计算规则为原购份数减已完成退款份数,最小为零。
+ */
+ private Integer effectiveProductCount;
+
/** 统计结果所属商户 ID,不作为购物车持久化字段。 */
private Long merchantId;
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/hard/HardApiController.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/hard/HardApiController.java
index 02d14e94..d5f55295 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/hard/HardApiController.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/hard/HardApiController.java
@@ -11,10 +11,12 @@ import com.bsd.pjcloud.common.security.util.SecurityUtils;
import com.bsd.pjcloud.hard.api.constant.HardProductNatureEnum;
import com.bsd.pjcloud.hard.api.constant.HardTypeEnum;
import com.bsd.pjcloud.hard.api.constant.order.HardPayTypeEnum;
+import com.bsd.pjcloud.hard.api.constant.order.HardOrderTypeEnum;
import com.bsd.pjcloud.hard.api.dto.async.SyncOfflineReqVo;
import com.bsd.pjcloud.hard.api.dto.hard.*;
import com.bsd.pjcloud.hard.api.dto.order.HardChartLatitudeVo;
import com.bsd.pjcloud.hard.api.dto.order.HardCreateCarDto;
+import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity;
import com.bsd.pjcloud.hard.api.vo.HardInvoiceVo;
import com.bsd.pjcloud.hard.api.vo.manager.HardManagerVo;
import com.bsd.pjcloud.hard.api.vo.android.HardAdUserVo;
@@ -37,6 +39,7 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
+import java.math.RoundingMode;
import java.util.List;
@RestController
@@ -96,9 +99,63 @@ public class HardApiController {
}
@GetMapping("/get-by-order-id")
- @Operation(summary = "根据订单id查询购物车详情")
+ @Operation(summary = "根据订单id查询终端待履约购物车详情")
public R> getByOrderId(@RequestParam Long orderId) {
- return R.ok(hardCarService.getByOrderId(orderId));
+ List carList = hardCarService.getByOrderId(orderId);
+ HardOrderEntity order = hardwareOrderService.getById(orderId);
+ return R.ok(projectFulfillableReservationCars(order, carList));
+ }
+
+ /**
+ * 食堂预约核销只向消费机暴露退款成功后的剩余商品;其他订单继续保持原接口数量和金额语义。
+ *
+ * App 订单详情使用独立接口,仍可同时读取原购、已退和剩余数量。这里覆盖旧版食堂、超市消费机
+ * 共用的二维码核销接口,避免终端继续按原购物车数量出餐。
+ *
+ * @param order 订单快照,用于限定食堂预约场景
+ * @param carList 带退款数量统计的购物车展示对象
+ * @return 终端可继续履约的商品;非食堂预约订单原样返回
+ */
+ private List projectFulfillableReservationCars(HardOrderEntity order, List carList) {
+ if (ObjectUtil.isEmpty(carList) || order == null
+ || !ObjectUtil.equals(order.getSysType(), HardTypeEnum.CANTEEN.getType())
+ || !ObjectUtil.equals(order.getOrderType(), HardOrderTypeEnum.RESERVE.getType())) {
+ return carList;
+ }
+ return carList.stream().filter(ObjectUtil::isNotEmpty).filter(car -> resolveEffectiveCount(car) > 0).peek(car -> {
+ int originalCount = resolveOriginalCount(car);
+ int effectiveCount = resolveEffectiveCount(car);
+ car.setProductCount(effectiveCount);
+ if (car.getTotalPrice() != null && originalCount > 0 && effectiveCount < originalCount) {
+ long effectiveTotalPrice = BigDecimal.valueOf(car.getTotalPrice())
+ .multiply(BigDecimal.valueOf(effectiveCount))
+ .divide(BigDecimal.valueOf(originalCount), 0, RoundingMode.HALF_UP)
+ .longValue();
+ car.setTotalPrice(effectiveTotalPrice);
+ }
+ }).toList();
+ }
+
+ /**
+ * 解析购物车原购份数,旧响应缺少显式快照字段时回退持久化数量。
+ *
+ * @param car 购物车展示对象
+ * @return 非负原购份数
+ */
+ private int resolveOriginalCount(HardCarVo car) {
+ Integer count = car.getOriginalProductCount() == null ? car.getProductCount() : car.getOriginalProductCount();
+ return count == null ? 0 : Math.max(0, count);
+ }
+
+ /**
+ * 解析退款成功后的剩余可履约份数,旧数据缺少统计字段时回退原购份数。
+ *
+ * @param car 购物车展示对象
+ * @return 非负剩余份数
+ */
+ private int resolveEffectiveCount(HardCarVo car) {
+ Integer count = car.getEffectiveProductCount() == null ? resolveOriginalCount(car) : car.getEffectiveProductCount();
+ return count == null ? 0 : Math.max(0, count);
}
@GetMapping("/get-order")
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardAfterSaleCarMapper.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardAfterSaleCarMapper.java
index 5a374397..b9312e94 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardAfterSaleCarMapper.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardAfterSaleCarMapper.java
@@ -7,6 +7,9 @@ import org.apache.ibatis.annotations.Param;
import java.util.List;
+/**
+ * 售后商品快照 Mapper,负责按订单父购物车和售后状态聚合退款占用量。
+ */
@Mapper
public interface HardAfterSaleCarMapper extends PjcloudBaseMapper {
@@ -19,6 +22,16 @@ public interface HardAfterSaleCarMapper extends PjcloudBaseMapper getOccupiedByCarParentId(@Param("carParentId") Long carParentId, @Param("statuses") List statuses);
+ /**
+ * 批量查询父购物车在指定售后状态下的商品占用汇总,结果按父购物车和商品 ID 分组。
+ *
+ * @param carParentIds 父购物车 ID 集合
+ * @param statuses 计入占用的售后状态编码集合
+ * @return 每个父购物车下按商品 ID 聚合的占用明细,无匹配数据时返回空列表
+ */
+ List getOccupiedByCarParentIds(@Param("carParentIds") List carParentIds,
+ @Param("statuses") List statuses);
+
/**
* 查询指定售后单保存的商品快照,审核同意时据此退库存。
*
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardOrderMapper.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardOrderMapper.java
index d88ad49b..bcf822aa 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardOrderMapper.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardOrderMapper.java
@@ -27,7 +27,7 @@ import java.util.List;
public interface HardOrderMapper extends PjcloudBaseMapper {
/**
- * 按主键查询并锁定订单行,供退款创建、审核、取消和后台直退串行化同一订单。
+ * 按主键查询并锁定订单行,供退款创建、审核、取消、后台直退和预约核销串行化同一订单。
*
* @param id 订单主键
* @return 锁定后的订单;不存在时返回 {@code null}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardAfterSaleCarService.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardAfterSaleCarService.java
index 2151d427..77f024c4 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardAfterSaleCarService.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardAfterSaleCarService.java
@@ -4,9 +4,14 @@ import com.baomidou.mybatisplus.extension.service.IService;
import com.bsd.pjcloud.hard.api.entity.order.HardAfterSaleCarEntity;
import com.bsd.pjcloud.hard.api.entity.order.HardAfterSaleEntity;
import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity;
+import com.bsd.pjcloud.hard.api.vo.order.HardCarVo;
import java.util.List;
+import java.util.Map;
+/**
+ * 售后商品快照与订单商品退款数量聚合服务。
+ */
public interface HardAfterSaleCarService extends IService {
/**
@@ -42,6 +47,21 @@ public interface HardAfterSaleCarService extends IService getOccupiedByCarParentId(Long carParentId, List statuses);
+ /**
+ * 按售后状态为订单购物车展示对象补充原购、已退、申请中和剩余份数。
+ *
+ * @param carParentId 原订单父购物车 ID
+ * @param carList 原订单购物车展示对象;原始 {@code productCount} 不会被修改
+ */
+ void fillRefundQuantityStats(Long carParentId, List carList);
+
+ /**
+ * 批量补充多个订单购物车的退款数量统计,整个列表固定执行两次售后聚合查询。
+ *
+ * @param carListsByParentId 父购物车 ID 到原订单购物车展示对象的映射
+ */
+ void fillRefundQuantityStats(Map> carListsByParentId);
+
/**
* 查询单个售后申请保存的商品快照。
*
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/ReservationMenuQueryServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/ReservationMenuQueryServiceImpl.java
index bb227926..aa980c5f 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/ReservationMenuQueryServiceImpl.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/ReservationMenuQueryServiceImpl.java
@@ -27,6 +27,7 @@ import com.bsd.pjcloud.hard.api.vo.android.ReservationMenuQueryVo;
import com.bsd.pjcloud.hard.api.vo.order.HardCarVo;
import com.bsd.pjcloud.hard.api.vo.order.HardOrderVo;
import com.bsd.pjcloud.hard.service.HardApiService;
+import com.bsd.pjcloud.hard.service.HardCarService;
import com.bsd.pjcloud.hard.service.HardManagerService;
import com.bsd.pjcloud.hard.service.HardOrderService;
import com.bsd.pjcloud.hard.service.ReservationMenuQueryService;
@@ -54,6 +55,10 @@ public class ReservationMenuQueryServiceImpl implements ReservationMenuQueryServ
private final HardManagerService hardManagerService;
private final HardApiService hardApiService;
private final HardOrderService hardOrderService;
+
+ /** 订单购物车服务,用于在直接核销接口中再次确认至少存在一份可履约商品。 */
+ private final HardCarService hardCarService;
+
private final RemoteUserService remoteUserService;
private final RemoteDeptService remoteDeptService;
private final RemoteMchService remoteMchService;
@@ -61,6 +66,7 @@ public class ReservationMenuQueryServiceImpl implements ReservationMenuQueryServ
/** 食堂菜品远程服务,用于批量补充订单商品类型和套餐组成。 */
private final RemoteCanteenSpuService remoteCanteenSpuService;
+ /** {@inheritDoc} */
/** {@inheritDoc} */
@Override
public ReservationMenuQueryVo query(ReservationMenuQueryDto dto) {
@@ -72,7 +78,9 @@ public class ReservationMenuQueryServiceImpl implements ReservationMenuQueryServ
query.setMerchantId(terminalMerchant.merchant().getId());
query.setOrderType(HardOrderTypeEnum.RESERVE.getType());
query.setOrderStatusCodes(List.of(HardOrderStatusEnum.PAY.getType()));
- query.setRefundStatusCodes(List.of(HardRefundStatusEnum.NONE.getStatus(), HardRefundStatusEnum.REJECT.getStatus()));
+ // 部分退款成功时订单仍为已支付并可继续核销,只有全额退款会由订单状态过滤掉。
+ query.setRefundStatusCodes(List.of(HardRefundStatusEnum.NONE.getStatus(), HardRefundStatusEnum.REJECT.getStatus(),
+ HardRefundStatusEnum.SUCCESS.getStatus()));
query.setApptIdOrApptRuleIdNotNull(true);
query.setApptUseDateStart(LocalDate.now().atStartOfDay());
query.setIsFindCarList(true);
@@ -84,11 +92,13 @@ public class ReservationMenuQueryServiceImpl implements ReservationMenuQueryServ
return aggregate(recognizedUser, terminalMerchant, orders, dto.getMaskName(), productMetadata);
}
+ /** {@inheritDoc} */
@Override
public Boolean cancel(ReservationMenuCancelDto dto) {
return cancel(dto, null);
}
+ /** {@inheritDoc} */
@Override
public Boolean cancel(ReservationMenuCancelDto dto, Long hardId) {
HardOrderEntity order = hardOrderService.getById(dto.getOrderId());
@@ -98,11 +108,14 @@ public class ReservationMenuQueryServiceImpl implements ReservationMenuQueryServ
throw new RuntimeException("该预约订单当前不可核销");
}
if (!isCancelableRefundStatus(order.getRefundStatus())) {
- throw new RuntimeException("该预约订单正在退款或已退款,不能核销");
+ throw new RuntimeException("该预约订单正在退款,不能核销");
}
if (order.getApptUseDate() == null || order.getApptUseDate().isBefore(LocalDate.now())) {
throw new RuntimeException("该预约订单已失效");
}
+ if (!hasFulfillableProduct(order.getId())) {
+ throw new RuntimeException("该预约订单商品已全部退款,不能核销");
+ }
HandCancelCodeDto cancelDto = new HandCancelCodeDto();
cancelDto.setOrderId(order.getId());
@@ -111,6 +124,17 @@ public class ReservationMenuQueryServiceImpl implements ReservationMenuQueryServ
return hardOrderService.handCancelCode(cancelDto);
}
+ /**
+ * 校验订单至少还有一份退款成功后可履约的商品,防止旧终端绕过查询结果直接核销空订单。
+ *
+ * @param orderId 待核销预约订单 ID
+ * @return 存在剩余商品时返回 {@code true}
+ */
+ private boolean hasFulfillableProduct(Long orderId) {
+ List cars = hardCarService.getByOrderId(orderId);
+ return cars != null && cars.stream().anyMatch(car -> resolveEffectiveProductCount(car) > 0);
+ }
+
private Long resolveVerificationHardId(HardOrderEntity order, Long hardId) {
if (hardId == null) return -1L;
HardManagerEntity hard = hardManagerService.getById(hardId);
@@ -120,10 +144,17 @@ public class ReservationMenuQueryServiceImpl implements ReservationMenuQueryServ
return hard.getId();
}
+ /**
+ * 判断预约订单退款聚合状态是否允许继续履约,历史空值按未售后兼容,未知状态按不可核销处理。
+ *
+ * @param refundStatus 订单退款聚合状态编码
+ * @return 未售后、退款拒绝或部分退款成功时返回 {@code true}
+ */
private static boolean isCancelableRefundStatus(String refundStatus) {
return StrUtil.isBlank(refundStatus)
|| Objects.equals(refundStatus, HardRefundStatusEnum.NONE.getStatus())
- || Objects.equals(refundStatus, HardRefundStatusEnum.REJECT.getStatus());
+ || Objects.equals(refundStatus, HardRefundStatusEnum.REJECT.getStatus())
+ || Objects.equals(refundStatus, HardRefundStatusEnum.SUCCESS.getStatus());
}
private TerminalMerchant resolveTerminalMerchant(Long hardId) {
@@ -263,27 +294,28 @@ public class ReservationMenuQueryServiceImpl implements ReservationMenuQueryServ
Map> grouped = new TreeMap<>();
for (HardOrderVo order : orders) {
+ List fulfillableItems = new ArrayList<>();
+ if (order.getCarList() != null) {
+ for (HardCarVo car : order.getCarList()) {
+ int effectiveCount = resolveEffectiveProductCount(car);
+ if (effectiveCount <= 0 || StrUtil.isBlank(car.getProductName())) continue;
+ // 历史购物车允许商品 ID 为空,不应拿空键访问 Map.of() 这类不可变映射。
+ ProductMetadata metadata = car.getProductId() == null ? null : productMetadata.get(car.getProductId());
+ fulfillableItems.add(new OrderItemAccumulator(
+ car.getProductId(), car.getProductName().trim(), car.getProductPic(), effectiveCount,
+ metadata == null ? 1 : metadata.spuType(),
+ metadata == null ? List.of() : metadata.comboItems()));
+ }
+ }
+ // 全部商品退款后不能只留下一个空订单核销入口,订单和餐段都应从结果中消失。
+ if (fulfillableItems.isEmpty()) continue;
String hourType = resolveHourType(order);
LinkedHashMap meals = grouped.computeIfAbsent(order.getApptUseDate(), ignored -> new LinkedHashMap<>());
MealAccumulator meal = meals.computeIfAbsent(hourType, ignored -> new MealAccumulator(hourType, resolveMealTitle(order, hourType)));
- List orderItems = null;
+ fulfillableItems.forEach(item -> meal.items().add(item.productName()));
if (order.getId() != null) {
meal.orderIds().add(order.getId());
- orderItems = meal.orderItems().computeIfAbsent(order.getId(), ignored -> new ArrayList<>());
- }
- if (order.getCarList() == null) continue;
- for (HardCarVo car : order.getCarList()) {
- if (car != null && StrUtil.isNotBlank(car.getProductName())) {
- String productName = car.getProductName().trim();
- meal.items().add(productName);
- if (orderItems != null) {
- ProductMetadata metadata = productMetadata.get(car.getProductId());
- orderItems.add(new OrderItemAccumulator(
- car.getProductId(), productName, car.getProductPic(), car.getProductCount(),
- metadata == null ? 1 : metadata.spuType(),
- metadata == null ? List.of() : metadata.comboItems()));
- }
- }
+ meal.orderItems().put(order.getId(), fulfillableItems);
}
}
@@ -309,6 +341,18 @@ public class ReservationMenuQueryServiceImpl implements ReservationMenuQueryServ
return result;
}
+ /**
+ * 解析退款成功后的剩余可履约份数;旧服务数据缺少统计字段时回退原购买数量。
+ *
+ * @param car 订单购物车展示对象
+ * @return 可出现在终端待核销菜单中的非负份数
+ */
+ private static int resolveEffectiveProductCount(HardCarVo car) {
+ if (car == null) return 0;
+ Integer count = car.getEffectiveProductCount() == null ? car.getProductCount() : car.getEffectiveProductCount();
+ return count == null ? 0 : Math.max(0, count);
+ }
+
/**
* 将单个餐段聚合结果转换为终端响应,并保持订单及商品的稳定顺序。
*
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardAfterSaleCarServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardAfterSaleCarServiceImpl.java
index 11ecfed6..539dc118 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardAfterSaleCarServiceImpl.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardAfterSaleCarServiceImpl.java
@@ -6,6 +6,7 @@ import com.bsd.pjcloud.hard.api.entity.order.HardAfterSaleCarEntity;
import com.bsd.pjcloud.hard.api.entity.order.HardAfterSaleEntity;
import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity;
import com.bsd.pjcloud.hard.api.constant.refund.HardSaleStatusEnum;
+import com.bsd.pjcloud.hard.api.vo.order.HardCarVo;
import com.bsd.pjcloud.hard.mapper.order.HardAfterSaleCarMapper;
import com.bsd.pjcloud.hard.mapper.order.HardOrderMapper;
import com.bsd.pjcloud.hard.service.HardAfterSaleCarService;
@@ -16,10 +17,12 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
/**
- * 购物车退款表
+ * 售后商品快照服务实现,保留原购物车数量并为各消费端补充退款后的只读数量口径。
*/
@Validated
@Slf4j
@@ -27,6 +30,7 @@ import java.util.List;
@Service
public class HardAfterSaleCarServiceImpl extends ServiceImpl implements HardAfterSaleCarService {
+ /** 订单 Mapper,用于从订单 ID 解析其父购物车 ID。 */
private final HardOrderMapper hardOrderMapper;
@Override
@@ -68,6 +72,101 @@ public class HardAfterSaleCarServiceImpl extends ServiceImpl同一商品存在多行历史快照时按购物车顺序分配累计退款份数,避免把商品级汇总重复写入每一行。
+ *
+ * @param carParentId 原订单父购物车 ID
+ * @param carList 原订单购物车展示对象;方法只写入非持久化统计字段
+ */
+ @Override
+ public void fillRefundQuantityStats(Long carParentId, List carList) {
+ if (carParentId == null || ObjectUtil.isEmpty(carList)) return;
+ fillRefundQuantityStats(Map.of(carParentId, carList));
+ }
+
+ /**
+ * 批量统计避免订单列表按每笔订单额外执行两次售后查询;每个购物车仍独立分配商品级累计份数。
+ *
+ * @param carListsByParentId 父购物车 ID 到原订单购物车展示对象的映射
+ */
+ @Override
+ public void fillRefundQuantityStats(Map> carListsByParentId) {
+ if (ObjectUtil.isEmpty(carListsByParentId)) return;
+ List parentIds = carListsByParentId.keySet().stream().filter(java.util.Objects::nonNull).distinct().toList();
+ if (parentIds.isEmpty()) return;
+ Map> refundedByParent = quantityMap(parentIds, HardSaleStatusEnum.COMPLETE.getStatus());
+ Map> applyingByParent = quantityMap(parentIds, HardSaleStatusEnum.APPLY.getStatus());
+ carListsByParentId.forEach((parentId, carList) -> fillCarQuantityStats(carList,
+ new LinkedHashMap<>(refundedByParent.getOrDefault(parentId, Map.of())),
+ new LinkedHashMap<>(applyingByParent.getOrDefault(parentId, Map.of()))));
+ }
+
+ /**
+ * 为同一父购物车的每行商品分配累计售后份数,重复商品行不会重复消费同一份退款。
+ *
+ * @param carList 原订单购物车展示对象
+ * @param refundedQuantities 尚未分配的已退款商品份数
+ * @param applyingQuantities 尚未分配的申请中商品份数
+ */
+ private void fillCarQuantityStats(List carList, Map refundedQuantities,
+ Map applyingQuantities) {
+ if (ObjectUtil.isEmpty(carList)) return;
+ for (HardCarVo car : carList) {
+ int originalCount = safeCount(car.getProductCount());
+ int refundedCount = takeQuantity(refundedQuantities, car.getProductId(), originalCount);
+ int applyingCount = takeQuantity(applyingQuantities, car.getProductId(), originalCount - refundedCount);
+ car.setOriginalProductCount(originalCount);
+ car.setRefundedProductCount(refundedCount);
+ car.setApplyingProductCount(applyingCount);
+ car.setEffectiveProductCount(Math.max(0, originalCount - refundedCount));
+ }
+ }
+
+ /**
+ * 查询单一售后状态下各商品累计份数,并转换为后续可扣减的可变映射。
+ *
+ * @param carParentIds 原订单父购物车 ID 集合
+ * @param status 售后状态协议编码
+ * @return 父购物车 ID 到商品累计份数映射,异常负数按零处理
+ */
+ private Map> quantityMap(List carParentIds, String status) {
+ Map> result = new LinkedHashMap<>();
+ for (HardAfterSaleCarEntity item : baseMapper.getOccupiedByCarParentIds(carParentIds, List.of(status))) {
+ if (item.getParentCarId() == null || item.getProductId() == null) continue;
+ result.computeIfAbsent(item.getParentCarId(), ignored -> new LinkedHashMap<>())
+ .merge(item.getProductId(), safeCount(item.getProductCount()), Integer::sum);
+ }
+ return result;
+ }
+
+ /**
+ * 从商品级累计份数中为当前购物车行领取不超过本行上限的数量,并保存剩余值供重复商品行继续领取。
+ *
+ * @param quantities 商品 ID 到尚未分配份数的映射
+ * @param productId 当前购物车商品 ID
+ * @param limit 当前行最多允许分配的份数
+ * @return 分配给当前购物车行的非负份数
+ */
+ private int takeQuantity(Map quantities, Long productId, int limit) {
+ if (productId == null || limit <= 0) return 0;
+ int remaining = Math.max(0, quantities.getOrDefault(productId, 0));
+ int taken = Math.min(limit, remaining);
+ quantities.put(productId, remaining - taken);
+ return taken;
+ }
+
+ /**
+ * 将数据库可空商品份数转换为非负整数,防止历史异常值扩大剩余数量。
+ *
+ * @param count 数据库商品份数
+ * @return 可参与退款统计的非负份数
+ */
+ private int safeCount(Integer count) {
+ return count == null ? 0 : Math.max(0, count);
+ }
+
/**
* 审核与详情展示只读取申请时保存的商品快照,避免依赖已变化的商品主数据。
*
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCarServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCarServiceImpl.java
index 4e2aee2b..0c37f307 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCarServiceImpl.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCarServiceImpl.java
@@ -29,6 +29,7 @@ import com.bsd.pjcloud.hard.api.vo.order.HardCarVo;
import com.bsd.pjcloud.hard.mapper.order.HardCarMapper;
import com.bsd.pjcloud.hard.mapper.order.HardOrderMapper;
import com.bsd.pjcloud.hard.mapper.order.HardParentCarMapper;
+import com.bsd.pjcloud.hard.service.HardAfterSaleCarService;
import com.bsd.pjcloud.hard.service.HardCarService;
import com.bsd.pjcloud.hard.service.HardSpuService;
import com.bsd.pjcloud.supermarket.api.feign.RemoteMallProductService;
@@ -75,6 +76,9 @@ public class HardCarServiceImpl extends ServiceImpl list = getByParentCarId(order.getParentCarId());
List result = BeanUtils.toBean(list, HardCarVo.class);
fillAppProductDetailMetadata(order.getParentCarId(), result);
+ hardAfterSaleCarService.fillRefundQuantityStats(order.getParentCarId(), result);
return result;
}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java
index 994f6550..e8b94040 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java
@@ -48,6 +48,7 @@ import com.bsd.pjcloud.hard.api.constant.HardTypeEnum;
import com.bsd.pjcloud.hard.api.constant.order.HardOrderStatusEnum;
import com.bsd.pjcloud.hard.api.constant.order.HardOrderTypeEnum;
import com.bsd.pjcloud.hard.api.constant.order.HardPayTypeEnum;
+import com.bsd.pjcloud.hard.api.constant.refund.HardRefundStatusEnum;
import com.bsd.pjcloud.hard.api.constant.xfj.HardXfjTypeEnum;
import com.bsd.pjcloud.hard.api.dto.hard.HandCancelActCodeDto;
import com.bsd.pjcloud.hard.api.dto.hard.HardDoneHardQrCodeScanOrderDto;
@@ -182,6 +183,8 @@ public class HardOrderServiceImpl extends ServiceImpl carList = hardCarService.getByParentCarId(detail.getParentCarId());
List carVoList = BeanUtils.toBean(carList, HardCarVo.class);
+ hardAfterSaleCarService.fillRefundQuantityStats(detail.getParentCarId(), carVoList);
detail.setCarList(carVoList);
});
// 预约订单
@@ -3415,12 +3419,19 @@ public class HardOrderServiceImpl extends ServiceImpl申请中的商品仍属于原订单履约资源,但为避免退款与出餐同时成功,整笔订单在申请处理期间暂停核销;
+ * 部分退款完成后只要仍有剩余商品即可继续核销。其他订单类型保持通用核销接口的历史行为。
+ *
+ * @param order 已持有数据库行锁的订单快照
+ */
+ private void validateCanteenReservationBeforeCancel(HardOrderEntity order) {
+ boolean isCanteenReservation = ObjectUtil.equals(order.getSysType(), HardTypeEnum.CANTEEN.getType())
+ && ObjectUtil.equals(order.getOrderType(), HardOrderTypeEnum.RESERVE.getType());
+ if (!isCanteenReservation) return;
+ if (!ObjectUtil.equals(order.getOrderStatus(), HardOrderStatusEnum.PAY.getType())) {
+ throw new RuntimeException("该预约订单当前不可核销");
+ }
+ if (!isReservationRefundStatusFulfillable(order.getRefundStatus())) {
+ throw new RuntimeException("该预约订单正在退款,不能核销");
+ }
+ List cars = hardCarService.getByOrderId(order.getId());
+ boolean hasFulfillableProduct = ObjectUtil.isNotEmpty(cars)
+ && cars.stream().anyMatch(car -> resolveEffectiveProductCount(car) > 0);
+ if (!hasFulfillableProduct) {
+ throw new RuntimeException("该预约订单商品已全部退款,不能核销");
+ }
+ }
+
+ /**
+ * 判断订单聚合退款状态是否允许预约履约;未知状态按不可核销处理,避免未来状态被旧终端误放行。
+ *
+ * @param refundStatus 订单退款聚合状态,历史空值等同未售后
+ * @return 未售后、退款拒绝或部分退款成功时返回 {@code true}
+ */
+ private boolean isReservationRefundStatusFulfillable(String refundStatus) {
+ return StrUtil.isBlank(refundStatus)
+ || ObjectUtil.equals(refundStatus, HardRefundStatusEnum.NONE.getStatus())
+ || ObjectUtil.equals(refundStatus, HardRefundStatusEnum.REJECT.getStatus())
+ || ObjectUtil.equals(refundStatus, HardRefundStatusEnum.SUCCESS.getStatus());
+ }
+
+ /**
+ * 解析购物车退款成功后的剩余可履约份数,兼容历史响应只包含原购买数量的情况。
+ *
+ * @param car 带售后统计的订单商品快照
+ * @return 非负剩余份数
+ */
+ private int resolveEffectiveProductCount(HardCarVo car) {
+ if (car == null) return 0;
+ Integer count = car.getEffectiveProductCount() == null ? car.getProductCount() : car.getEffectiveProductCount();
+ return count == null ? 0 : Math.max(0, count);
+ }
+
/**
* 在预约订单PAY到CANCEL状态转换的同一本地事务内保存用餐核销消息快照。
*
@@ -4249,11 +4311,14 @@ public class HardOrderServiceImpl extends ServiceImpl findCarListFuture = CompletableFuture.runAsync(() -> {
if (dto.getIsFindCarList()) {
+ Map> carListsByParentId = new LinkedHashMap<>();
for (HardOrderVo v : list) {
List carList = hardCarService.getByParentCarId(v.getParentCarId());
List carVoList = BeanUtils.toBean(carList, HardCarVo.class);
+ if (v.getParentCarId() != null) carListsByParentId.put(v.getParentCarId(), carVoList);
v.setCarList(carVoList);
}
+ hardAfterSaleCarService.fillRefundQuantityStats(carListsByParentId);
}
});
// 预约订单
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardAfterSaleCarMapper.xml b/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardAfterSaleCarMapper.xml
index 33ffb43b..d4bb1874 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardAfterSaleCarMapper.xml
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardAfterSaleCarMapper.xml
@@ -84,6 +84,32 @@
group by a.parent_car_id, a.product_id
+
+