diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderDto.java index 45ed8c735..56a07c424 100644 --- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderDto.java +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderDto.java @@ -81,6 +81,12 @@ public class HardOrderDto extends HardOrderEntity implements BaseIsIncludeTreeIn /** 预约用餐日期下限,用于查询今天及之后的未核销预约。 */ private LocalDateTime apptUseDateStart; + /** + * 统一用餐日期范围:预约单按预约使用日匹配,线下等其他类型按支付时间匹配; + * 仅 ward 系列用餐接口使用。 + */ + private LocalDateTime[] mealDateRange; + private List orderTypeCodes; private List orderPayTypeCodes; diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderVo.java index 407e340aa..c02b50c41 100644 --- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderVo.java +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderVo.java @@ -133,6 +133,12 @@ public class HardOrderVo extends HardOrderEntity implements BaseUserDeptInterfac */ private Boolean canViewReserveCode; + /** + * 统一用餐日期(yyyy-MM-dd):预约单取预约使用日,线下等其他类型取支付日; + * 仅 ward 系列用餐接口回填,不对应 hard_order 表持久化字段。 + */ + private String mealDate; + /** * 订单详情中的长期识别证据列表,按证据类型、角色和序号排序。 */ diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/WardMenuMapDayVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/WardMenuMapDayVo.java index 6bfdb8956..5785e18c3 100644 --- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/WardMenuMapDayVo.java +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/WardMenuMapDayVo.java @@ -6,10 +6,10 @@ import lombok.Data; import java.time.LocalDate; /** - * 预约菜谱日历地图的单日聚合:按用餐日统计未退款预约订单的核销情况。 + * 用餐日历地图的单日聚合:按统一用餐日统计未退款订单的核销与消费情况。 */ @Data -@Schema(description = "预约菜谱日历地图单日聚合") +@Schema(description = "用餐日历地图单日聚合") public class WardMenuMapDayVo { @Schema(description = "用餐日") @@ -20,4 +20,7 @@ public class WardMenuMapDayVo { @Schema(description = "已核销预约单数(订单状态 20)") private Integer verifiedCount; + + @Schema(description = "线下等其他类型订单数(预约外全类型,已支付口径)") + private Integer offlineCount; } diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderController.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderController.java index d6eae4c35..9f0a84891 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderController.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderController.java @@ -177,7 +177,7 @@ public class HardOrderController { return R.ok(orders); } - @Operation(summary = "app查询预约菜谱日历地图", description = "按日期范围聚合本人或被监护人每天的预约核销情况,整单退款的订单不展示") + @Operation(summary = "app查询用餐日历地图", description = "按日期范围聚合本人或被监护人每天的用餐情况:预约待核销/已核销与线下等其他类型订单数,整单退款的订单不统计") @ApiCryptoAes @GetMapping("/app/ward-menu-map") public R> wardMenuMap(@RequestParam(required = false) List dinerUserIds, @@ -190,22 +190,46 @@ public class HardOrderController { : hardwareOrderService.validateReserveDinerUserIds(user.getId(), dinerUserIds)); dto.setIsFindCarList(false); dto.setIsFindApptList(true); - dto.setApptIdOrApptRuleIdNotNull(true); - dto.setOrderType(HardOrderTypeEnum.RESERVE.getType()); - // 只统计待核销(5)与已核销(20);退款中(10)、已取消(30)和未支付订单不进入地图。 - dto.setOrderStatusCodes(List.of(HardOrderStatusEnum.PAY.getType(), HardOrderStatusEnum.CANCEL.getType())); - dto.setApptUseDateTimeList(new LocalDateTime[]{ + // 只统计待核销(5)、线下本地支付(6)与已核销(20);退款中(10)、已取消(30)和未支付订单不进入地图。 + dto.setOrderStatusCodes(List.of( + HardOrderStatusEnum.PAY.getType(), + HardOrderStatusEnum.OFFLINE_PAY.getType(), + HardOrderStatusEnum.CANCEL.getType())); + // 统一用餐日期口径:预约单按预约使用日匹配,线下等其他类型按支付时间匹配。 + dto.setMealDateRange(new LocalDateTime[]{ LocalDate.parse(startDate).atStartOfDay(), LocalDate.parse(endDate).atTime(23, 59, 59), }); return R.ok(hardwareOrderService.wardMenuMap(dto)); } - @Operation(summary = "app查询预约菜谱某日订单明细", description = "查询某天本人或被监护人的预约订单明细,整单退款的订单不返回") + @Operation(summary = "app查询用餐某日订单明细", description = "查询某天本人或被监护人的全类型用餐订单明细,整单退款的订单不返回") @ApiCryptoAes @GetMapping("/app/ward-menu-day-orders") public R> wardMenuDayOrders(@RequestParam(required = false) List dinerUserIds, @RequestParam String date) { + return R.ok(wardMenuOrdersResponse(dinerUserIds, date, date)); + } + + @Operation(summary = "app查询用餐订单窗口列表", description = "按日期窗口查询本人或被监护人的全类型用餐订单明细,整单退款的订单不返回") + @ApiCryptoAes + @GetMapping("/app/ward-menu-orders") + public R> wardMenuOrders(@RequestParam(required = false) List dinerUserIds, + @RequestParam String startDate, + @RequestParam String endDate) { + return R.ok(wardMenuOrdersResponse(dinerUserIds, startDate, endDate)); + } + + /** + * 组装 ward 系列用餐明细查询:校验查看对象后按统一用餐日期窗口查询全类型订单, + * 某日明细与日期窗口明细共用同一口径,避免两接口统计范围出现偏差。 + * + * @param dinerUserIds 查看对象用户 ID,空表示当前登录用户本人 + * @param startDate 用餐日期窗口起(含),格式 yyyy-MM-dd + * @param endDate 用餐日期窗口止(含),格式 yyyy-MM-dd + * @return 已回填统一用餐日期和操作权限的全类型用餐订单 + */ + private List wardMenuOrdersResponse(List dinerUserIds, String startDate, String endDate) { HardOrderDto dto = new HardOrderDto(); PjcloudUser user = SecurityUtils.getUser(); dto.setUserIds(ObjectUtil.isEmpty(dinerUserIds) @@ -213,17 +237,19 @@ public class HardOrderController { : hardwareOrderService.validateReserveDinerUserIds(user.getId(), dinerUserIds)); dto.setIsFindCarList(true); dto.setIsFindApptList(true); - dto.setApptIdOrApptRuleIdNotNull(true); - dto.setOrderType(HardOrderTypeEnum.RESERVE.getType()); - // 与日历地图同口径:只返回待核销(5)与已核销(20),整单退款在服务层过滤。 - dto.setOrderStatusCodes(List.of(HardOrderStatusEnum.PAY.getType(), HardOrderStatusEnum.CANCEL.getType())); - dto.setApptUseDateTimeList(new LocalDateTime[]{ - LocalDate.parse(date).atStartOfDay(), - LocalDate.parse(date).atTime(23, 59, 59), + // 与用餐日历地图同口径:待核销(5)、线下本地支付(6)、已核销(20),整单退款在服务层过滤。 + dto.setOrderStatusCodes(List.of( + HardOrderStatusEnum.PAY.getType(), + HardOrderStatusEnum.OFFLINE_PAY.getType(), + HardOrderStatusEnum.CANCEL.getType())); + // 统一用餐日期口径:预约单按预约使用日匹配,线下等其他类型按支付时间匹配。 + dto.setMealDateRange(new LocalDateTime[]{ + LocalDate.parse(startDate).atStartOfDay(), + LocalDate.parse(endDate).atTime(23, 59, 59), }); - List orders = hardwareOrderService.wardMenuDayOrders(dto); + List orders = hardwareOrderService.wardMenuOrders(dto); hardwareOrderService.applyAppPermissions(orders, user.getId()); - return R.ok(orders); + return orders; } diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderService.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderService.java index f449fe26b..012ea9436 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderService.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderService.java @@ -198,15 +198,20 @@ public interface HardOrderService extends IService { List validateReserveDinerUserIds(Long payerUserId, List dinerUserIds); /** - * app 预约菜谱日历地图:按用餐日聚合未退款预约订单的待核销/已核销数量。 + * app 用餐日历地图:按统一用餐日聚合未退款订单,预约单分待核销/已核销,其余类型计线下消费。 */ List wardMenuMap(HardOrderDto dto); /** - * app 预约菜谱某日明细:返回当天未整单退款的待核销/已核销预约订单。 + * app 用餐某日明细:返回当天未整单退款的全类型用餐订单,并回填统一用餐日期。 */ List wardMenuDayOrders(HardOrderDto dto); + /** + * app 用餐订单窗口列表:返回日期窗口内未整单退款的全类型用餐订单,并回填统一用餐日期。 + */ + List wardMenuOrders(HardOrderDto dto); + /** * app创建食堂外卖订单 */ 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 5231fe3a6..a0f924914 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 @@ -2466,41 +2466,71 @@ public class HardOrderServiceImpl extends ServiceImpl wardMenuMap(HardOrderDto dto) { List orders = scopeList(dto, null); Map dayMap = new TreeMap<>(); for (HardOrderVo order : orders) { - if (order.getApptUseDate() == null || isFullyRefundedOrder(order)) { + LocalDate mealDate = resolveOrderMealDate(order); + if (mealDate == null || isFullyRefundedOrder(order)) { continue; } - WardMenuMapDayVo day = dayMap.computeIfAbsent(order.getApptUseDate(), date -> { + WardMenuMapDayVo day = dayMap.computeIfAbsent(mealDate, date -> { WardMenuMapDayVo item = new WardMenuMapDayVo(); item.setApptUseDate(date); item.setPendingCount(0); item.setVerifiedCount(0); + item.setOfflineCount(0); return item; }); - if (HardOrderStatusEnum.PAY.getType().equals(order.getOrderStatus())) { - day.setPendingCount(day.getPendingCount() + 1); - } else if (HardOrderStatusEnum.CANCEL.getType().equals(order.getOrderStatus())) { - day.setVerifiedCount(day.getVerifiedCount() + 1); + if (HardOrderTypeEnum.RESERVE.getType().equals(order.getOrderType())) { + if (HardOrderStatusEnum.PAY.getType().equals(order.getOrderStatus())) { + day.setPendingCount(day.getPendingCount() + 1); + } else if (HardOrderStatusEnum.CANCEL.getType().equals(order.getOrderStatus())) { + day.setVerifiedCount(day.getVerifiedCount() + 1); + } + } else { + day.setOfflineCount(day.getOfflineCount() + 1); } } return new ArrayList<>(dayMap.values()); } /** - * app 预约菜谱某日明细:返回当天未整单退款的待核销/已核销预约订单,与日历地图同口径。 + * app 用餐某日明细:返回当天未整单退款的全类型用餐订单,并回填统一用餐日期,与日历地图同口径。 */ @Override public List wardMenuDayOrders(HardOrderDto dto) { - return scopeList(dto, null).stream() - .filter(order -> !isFullyRefundedOrder(order)) + return wardMenuOrders(dto); + } + + /** + * app 用餐订单窗口列表:返回统一用餐日期窗口内未整单退款的全类型用餐订单,并回填统一用餐日期。 + * 无法解析用餐日期的订单(预约缺使用日且无支付时间)不返回,避免前端落入脏数据兜底日。 + */ + @Override + public List wardMenuOrders(HardOrderDto dto) { + List orders = scopeList(dto, null).stream() + .filter(order -> resolveOrderMealDate(order) != null && !isFullyRefundedOrder(order)) .toList(); + for (HardOrderVo order : orders) { + order.setMealDate(resolveOrderMealDate(order).toString()); + } + return orders; + } + + /** + * 解析订单统一用餐日期:预约单取预约使用日,线下消费、外卖等其他类型取支付日; + * 两者皆缺失时返回 null,调用方按无法归日处理。 + */ + private LocalDate resolveOrderMealDate(HardOrderVo order) { + if (HardOrderTypeEnum.RESERVE.getType().equals(order.getOrderType()) && order.getApptUseDate() != null) { + return order.getApptUseDate(); + } + return order.getPayTime() == null ? null : order.getPayTime().toLocalDate(); } /** diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardOrderMapper.xml b/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardOrderMapper.xml index 2551b4907..f35a44774 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardOrderMapper.xml +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardOrderMapper.xml @@ -333,6 +333,19 @@ + + + + and ( + (a.order_type = '2' and a.appt_use_date between #{dto.mealDateRange[0]} and #{dto.mealDateRange[1]}) + or (a.order_type <> '2' and a.pay_time between #{dto.mealDateRange[0]} and #{dto.mealDateRange[1]}) + ) + + + @@ -431,6 +444,7 @@ + and m.type = #{dto.hardTypeCode} diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImplWardMenuTest.java b/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImplWardMenuTest.java new file mode 100644 index 000000000..8e3c58765 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImplWardMenuTest.java @@ -0,0 +1,152 @@ +package com.bsd.pjcloud.hard.service.impl.order; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.bsd.pjcloud.hard.api.constant.order.HardOrderStatusEnum; +import com.bsd.pjcloud.hard.api.constant.order.HardOrderTypeEnum; +import com.bsd.pjcloud.hard.api.dto.order.HardOrderDto; +import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity; +import com.bsd.pjcloud.hard.api.vo.order.HardOrderVo; +import com.bsd.pjcloud.hard.api.vo.order.WardMenuMapDayVo; +import com.bsd.pjcloud.hard.mapper.order.HardOrderMapper; +import com.bsd.pjcloud.hard.support.HardUserIdentitySupport; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Answers.CALLS_REAL_METHODS; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * 用餐日历地图与窗口明细测试:预约单按状态分待核销/已核销计数,线下等其他类型统一计入线下消费, + * 整单退款订单不统计,返回明细回填统一用餐日期。 + */ +class HardOrderServiceImplWardMenuTest { + + /** 订单 Mapper,scopeList 打桩返回构造订单,隔离 SQL 与展示装配逻辑。 */ + private final HardOrderMapper hardOrderMapper = mock(HardOrderMapper.class); + + /** 身份筛选支持,默认关闭身份条件避免测试触发用户主键预筛选。 */ + private final HardUserIdentitySupport userIdentitySupport = mock(HardUserIdentitySupport.class); + + /** 被测订单服务,仅执行真实方法。 */ + private final HardOrderServiceImpl service = mock(HardOrderServiceImpl.class, CALLS_REAL_METHODS); + + /** + * 为被测服务注入查询链路实际访问的基础依赖。 + */ + @BeforeEach + void setUp() { + TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), "test"), + HardOrderEntity.class); + ReflectionTestUtils.setField(service, "baseMapper", hardOrderMapper); + ReflectionTestUtils.setField(service, "userIdentitySupport", userIdentitySupport); + when(userIdentitySupport.hasIdentityQuery(any())).thenReturn(false); + } + + /** + * 同一天内:待核销预约、已核销预约、线下消费分别计数;整单退款订单不计入任何维度。 + */ + @Test + void wardMenuMapAggregatesReserveStatusAndOfflineCount() { + LocalDate mealDate = LocalDate.of(2026, 9, 18); + List orders = List.of( + order(HardOrderTypeEnum.RESERVE.getType(), HardOrderStatusEnum.PAY.getType(), mealDate, null, 1500, 0), + order(HardOrderTypeEnum.RESERVE.getType(), HardOrderStatusEnum.CANCEL.getType(), mealDate, null, 1200, 0), + order(HardOrderTypeEnum.BELOW.getType(), HardOrderStatusEnum.CANCEL.getType(), null, + LocalDateTime.of(2026, 9, 18, 12, 10), 800, 0), + // 整单退款:退款金额不低于实付金额,不进入地图 + order(HardOrderTypeEnum.BELOW.getType(), HardOrderStatusEnum.CANCEL.getType(), null, + LocalDateTime.of(2026, 9, 18, 18, 30), 900, 900)); + when(hardOrderMapper.scopeList(any(HardOrderDto.class), isNull())).thenReturn(orders); + + List days = service.wardMenuMap(new HardOrderDto()); + + assertEquals(1, days.size()); + WardMenuMapDayVo day = days.get(0); + assertEquals(mealDate, day.getApptUseDate()); + assertEquals(1, day.getPendingCount()); + assertEquals(1, day.getVerifiedCount()); + assertEquals(1, day.getOfflineCount()); + } + + /** + * 预约单按预约使用日归组,线下等其他类型按支付日归组,两类订单落在不同日期时分别出条目。 + */ + @Test + void wardMenuMapGroupsByUnifiedMealDate() { + List orders = List.of( + order(HardOrderTypeEnum.RESERVE.getType(), HardOrderStatusEnum.PAY.getType(), + LocalDate.of(2026, 9, 20), null, 1500, 0), + order(HardOrderTypeEnum.TAKE_OUT.getType(), HardOrderStatusEnum.PAY.getType(), null, + LocalDateTime.of(2026, 9, 17, 19, 0), 2000, 0)); + when(hardOrderMapper.scopeList(any(HardOrderDto.class), isNull())).thenReturn(orders); + + List days = service.wardMenuMap(new HardOrderDto()); + + assertEquals(2, days.size()); + assertEquals(LocalDate.of(2026, 9, 17), days.get(0).getApptUseDate()); + assertEquals(1, days.get(0).getOfflineCount()); + assertEquals(LocalDate.of(2026, 9, 20), days.get(1).getApptUseDate()); + assertEquals(1, days.get(1).getPendingCount()); + } + + /** + * 窗口明细回填统一用餐日期:预约单取预约使用日,线下等其他类型取支付日; + * 无法解析用餐日期的订单不返回。 + */ + @Test + void wardMenuOrdersFillsUnifiedMealDate() { + List orders = List.of( + order(HardOrderTypeEnum.RESERVE.getType(), HardOrderStatusEnum.PAY.getType(), + LocalDate.of(2026, 9, 20), null, 1500, 0), + order(HardOrderTypeEnum.BELOW.getType(), HardOrderStatusEnum.CANCEL.getType(), null, + LocalDateTime.of(2026, 9, 18, 12, 10), 800, 0), + // 无预约使用日且无支付时间的脏数据,不返回 + order(HardOrderTypeEnum.BELOW.getType(), HardOrderStatusEnum.CANCEL.getType(), null, null, 100, 0)); + when(hardOrderMapper.scopeList(any(HardOrderDto.class), isNull())).thenReturn(orders); + + List result = service.wardMenuOrders(new HardOrderDto()); + + assertEquals(2, result.size()); + assertTrue(result.stream().anyMatch(order -> "2026-09-20".equals(order.getMealDate()))); + assertTrue(result.stream().anyMatch(order -> "2026-09-18".equals(order.getMealDate()))); + } + + /** 测试订单 ID 序号,仅保证列表内 ID 唯一。 */ + private long orderIdSequence = 0L; + + /** + * 构造一笔用餐订单快照。 + * + * @param orderType 订单类型协议编码 + * @param orderStatus 订单状态协议编码 + * @param apptUseDate 预约使用日;非预约订单传 null + * @param payTime 支付完成时间;预约订单可传 null + * @param payPrice 实付金额,单位为分 + * @param refundPrice 累计已完成退款金额,单位为分 + * @return 可供聚合逻辑使用的订单 + */ + private HardOrderVo order(String orderType, String orderStatus, LocalDate apptUseDate, + LocalDateTime payTime, int payPrice, int refundPrice) { + HardOrderVo item = new HardOrderVo(); + item.setId(++orderIdSequence); + item.setOrderType(orderType); + item.setOrderStatus(orderStatus); + item.setApptUseDate(apptUseDate); + item.setPayTime(payTime); + item.setPayPrice(payPrice); + item.setRefundPrice(refundPrice); + return item; + } +}