From fc35443b90059ff70351afb488aa8cb1c20d1341 Mon Sep 17 00:00:00 2001 From: dzc <2388224167@qq.com> Date: Wed, 26 Aug 2026 17:49:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E9=92=B1=E5=8C=85=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=E9=83=BD=E6=94=B9=E4=B8=BA=E5=88=86=E6=89=B9?= =?UTF-8?q?=EF=BC=8C=E5=89=8D=E7=AB=AF=E6=98=BE=E7=A4=BA=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E8=BF=9B=E5=BA=A6=E6=9D=A1=E7=9A=84=E6=A8=A1=E5=BC=8F=E3=80=82?= =?UTF-8?q?=20fix:=E4=BF=AE=E6=94=B9=E5=90=84=E4=B8=AA=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E7=BB=9F=E8=AE=A1=E7=9A=84=E6=95=B0=E9=87=8F?= =?UTF-8?q?=E4=B8=8D=E4=B8=80=E8=87=B4=E7=9A=84=E6=83=85=E5=86=B5=E3=80=82?= =?UTF-8?q?=20fix:=E4=BF=AE=E6=94=B9=E6=94=AF=E4=BB=98=E5=AE=9D=E5=88=86?= =?UTF-8?q?=E8=B4=A6=E9=80=80=E6=AC=BE=E5=A4=B1=E8=B4=A5=E7=9A=84=E6=83=85?= =?UTF-8?q?=E5=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../order/HardCanteenUserApptQueryDto.java | 7 +- .../vo/order/HardCanteenUserApptOrderVo.java | 5 + .../api/vo/order/HardCanteenUserApptVo.java | 4 +- .../excel/HardCanteenUserApptExcelVo.java | 12 +- .../hard/mapper/order/HardCarMapper.java | 20 + .../model/HardCanteenReservationSaleFact.java | 39 + ...HardCanteenApptTotalExportServiceImpl.java | 151 +- .../HardCanteenApptTotalServiceImpl.java | 198 ++- .../resources/mapper/order/HardCarMapper.xml | 38 + .../ThirdAppPlusAliPayServiceImpl.java | 9 +- .../impl/appplus/ThirdAppPlusServiceImpl.java | 24 +- .../ThirdAppPlusAliPayServiceImplTest.java | 18 + ...irdAppPlusServiceImplOrderCodePayTest.java | 29 + .../vo/export/WalletManageExportExcelVo.java | 67 + .../WalletRechargeImportExportExcelVo.java | 71 + .../WalletRechargePersonExportExcelVo.java | 20 + .../WalletTransactionExportExcelVo.java | 71 + .../vo/export/WalletUserExportExcelVo.java | 54 + .../controller/WalletManageController.java | 10 +- .../WalletTransactionController.java | 168 ++- .../WalletRechargeImportController.java | 91 +- .../wallet/mapper/WalletManageMapper.java | 9 +- .../mapper/WalletTransactionMapper.java | 30 +- .../recharge/WalletRechargeImportMapper.java | 8 + .../wallet/service/WalletManageService.java | 23 + .../service/WalletRechargeImportService.java | 13 + .../service/WalletTransactionExportScene.java | 107 ++ .../service/WalletTransactionService.java | 25 + .../service/WalletUserExportService.java | 68 + .../service/impl/WalletManageServiceImpl.java | 72 +- .../impl/WalletTransactionServiceImpl.java | 57 +- .../export/WalletUserExportServiceImpl.java | 1249 +++++++++++++++++ .../WalletRechargeImportServiceImpl.java | 38 +- .../recharge/WalletRechargeImportMapper.xml | 2 +- 34 files changed, 2620 insertions(+), 187 deletions(-) create mode 100644 pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/model/HardCanteenReservationSaleFact.java create mode 100644 pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/vo/export/WalletManageExportExcelVo.java create mode 100644 pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/vo/export/WalletRechargeImportExportExcelVo.java create mode 100644 pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/vo/export/WalletRechargePersonExportExcelVo.java create mode 100644 pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/vo/export/WalletTransactionExportExcelVo.java create mode 100644 pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/vo/export/WalletUserExportExcelVo.java create mode 100644 pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/WalletTransactionExportScene.java create mode 100644 pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/WalletUserExportService.java create mode 100644 pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/export/WalletUserExportServiceImpl.java diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardCanteenUserApptQueryDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardCanteenUserApptQueryDto.java index d16c4ae45..b0c918667 100644 --- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardCanteenUserApptQueryDto.java +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardCanteenUserApptQueryDto.java @@ -22,10 +22,13 @@ import java.util.List; @Schema(description = "食堂用户预约查询条件") public class HardCanteenUserApptQueryDto implements BaseIsIncludeTreeInterface, BaseUserIdentityQueryInterface { - /** 用餐日期闭区间,固定为开始、结束两个服务端本地日期,业务层限制跨度最多为两个自然月。 */ + /** + * 统计日期闭区间,固定为开始、结束两个服务端本地日期,业务层限制跨度最多为两个自然月。 + * 核销状态为 VERIFIED 时匹配实际核销时间,其他状态匹配预约用餐日期。 + */ @NotNull(message = "用餐日期范围不能为空") @Size(min = 2, max = 2, message = "用餐日期范围必须包含开始和结束日期") - @Schema(description = "用餐日期范围,固定为开始日期和结束日期", requiredMode = Schema.RequiredMode.REQUIRED) + @Schema(description = "统计日期范围,已核销取核销日期,其他状态取预约用餐日期", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDate[] apptUseDateList; /** 用户姓名、登录账号或手机号的模糊查询关键字;为空时不限制。 */ diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptOrderVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptOrderVo.java index 9e12c4427..2f52cb63c 100644 --- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptOrderVo.java +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptOrderVo.java @@ -3,6 +3,7 @@ package com.bsd.pjcloud.hard.api.vo.order; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import java.time.LocalDate; import java.time.LocalDateTime; /** @@ -20,6 +21,10 @@ public class HardCanteenUserApptOrderVo { @Schema(description = "用餐时间段字典编码") private String hourType; + /** 订单预约用餐日期;已核销按核销日查询时用于保留原预约日期,历史未记录时为空。 */ + @Schema(description = "预约用餐日期") + private LocalDate apptUseDate; + /** 下单时保存的商品描述;历史数据未记录时为空。 */ @Schema(description = "订餐内容") private String productDesc; diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptVo.java index 032c03ccb..50718668a 100644 --- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptVo.java +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptVo.java @@ -92,8 +92,8 @@ public class HardCanteenUserApptVo implements BaseUserDeptInterface, BaseUserIde @Schema(description = "老师身份扩展信息") private SysUserTeacherDto teacherInfo; - /** 本行对应的用餐日期。 */ - @Schema(description = "用餐日期") + /** 本行统计日期;已核销筛选取实际核销日期,其他筛选取预约用餐日期。 */ + @Schema(description = "统计日期,已核销取核销日期,其他状态取预约用餐日期") private LocalDate apptUseDate; /** 订餐状态,RESERVED 表示当前筛选口径存在有效预约,NOT_RESERVED 表示不存在。 */ diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/excel/HardCanteenUserApptExcelVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/excel/HardCanteenUserApptExcelVo.java index c4cb50a6b..74fabef31 100644 --- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/excel/HardCanteenUserApptExcelVo.java +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/excel/HardCanteenUserApptExcelVo.java @@ -9,7 +9,7 @@ import java.math.BigDecimal; import java.time.LocalDate; /** - * 用户和用餐日期维度的食堂预约导出行。 + * 用户和统计日期维度的食堂预约导出行。 */ @Data @ColumnWidth(22) @@ -63,8 +63,8 @@ public class HardCanteenUserApptExcelVo { @ExcelProperty({"老师身份", "老师学号"}) private String teacherNo; - /** 查询指定的用餐日期。 */ - @ExcelProperty({"预约信息", "用餐日期"}) + /** 查询指定的统计日期;已核销筛选取核销日期,其他状态取预约用餐日期。 */ + @ExcelProperty({"预约信息", "统计日期"}) private LocalDate apptUseDate; /** 是否存在符合当前食堂、餐段筛选的有效预约。 */ @@ -103,12 +103,12 @@ public class HardCanteenUserApptExcelVo { @ExcelProperty({"金额统计", "餐段总金额(元)"}) private String mealSegmentTotalPrices; - /** 用户本行日期内有效预约商品总价,单位为元并保留两位小数。 */ - @ExcelProperty({"金额统计", "当日总金额(元)"}) + /** 用户本行统计日期内完成退款扣减后的预约商品净额,单位为元并保留两位小数。 */ + @ExcelProperty({"金额统计", "统计日净额(元)"}) @NumberFormat("#,##0.00") private BigDecimal totalPrice; - /** 用户当天有效预约的整体核销状态。 */ + /** 用户在本行统计日期和当前筛选口径内有效预约的整体核销状态。 */ @ExcelProperty({"核销信息", "核销状态"}) private String verificationStatus; diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardCarMapper.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardCarMapper.java index 9a1e5485a..26a42a9f3 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardCarMapper.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardCarMapper.java @@ -16,6 +16,7 @@ import com.bsd.pjcloud.hard.api.vo.order.HardCanteenProductApptExportRecord; import com.bsd.pjcloud.hard.api.vo.order.app.HardAppProductByCanteenVo; import com.bsd.pjcloud.hard.api.vo.order.app.HardAppProductByClassifyVo; import com.bsd.pjcloud.hard.api.vo.order.app.HardAppProductLikeTopVo; +import com.bsd.pjcloud.hard.mapper.order.model.HardCanteenReservationSaleFact; import com.github.yulichang.wrapper.MPJLambdaWrapper; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @@ -67,6 +68,14 @@ public interface HardCarMapper extends PjcloudBaseMapper { */ List productTotalByDeptStats(@Param("dto") HardOrderDto dto, DataScope dataScope); + /** + * 查询预约核销商品事实,复用菜品销售统计的核销时间和完成退款净额口径。 + * + * @param dto 核销日期、食堂、餐段和用户范围;调用方必须把来源限制为预约核销 + * @return 按核销时间、订单和商品稳定排序的有效商品事实,全额退款订单不返回记录 + */ + List listReservationVerifiedSaleFacts(@Param("dto") HardOrderDto dto); + Page productTotalByApptUseDate(Page page, @Param("dto") HardOrderDto dto, DataScope dataScope); /** @@ -133,6 +142,17 @@ public interface HardCarMapper extends PjcloudBaseMapper { .orderByAsc(HardCarEntity::getId)); } + /** + * 按父购物车批量查询完成退款扣减后的预约商品快照。 + * + *

部分退款仅保留剩余份数和金额,全额退款商品不返回;该结果用于待核销、部分核销及预约日统计, + * 避免人员页面继续累计原始购物车快照。

+ * + * @param parentCarIds 预约订单关联的父购物车主键集合 + * @return 退款后仍有有效数量的商品快照;传入空集合时调用方应直接返回空列表 + */ + List listEffectiveReservationCars(@Param("parentCarIds") List parentCarIds); + /** * 菜品销量按照分类查询 */ diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/model/HardCanteenReservationSaleFact.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/model/HardCanteenReservationSaleFact.java new file mode 100644 index 000000000..7ba31b460 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/model/HardCanteenReservationSaleFact.java @@ -0,0 +1,39 @@ +package com.bsd.pjcloud.hard.mapper.order.model; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 食堂预约核销商品事实。 + * + *

数据直接来自菜品销售统计的预约核销分支,日期取订单核销时间,数量和金额均已扣除状态为 50 的 + * 完成退款;仅用于 Hardware 服务内部统一人员统计与商品统计口径,不作为外部接口模型。

+ */ +@Data +public class HardCanteenReservationSaleFact { + + /** 订单实际核销完成时间,用于把人员统计行归入对应核销日期。 */ + private LocalDateTime statisticsTime; + + /** 已核销预约订单主键,用于订单去重及关联页面预约明细。 */ + private Long orderId; + + /** 实际用餐用户主键,用于人员筛选和人数去重。 */ + private Long userId; + + /** 订单餐段协议编码,来源于核销时的订单快照。 */ + private String hourType; + + /** 订单商品主键,历史数据无法识别商品时可为空。 */ + private Long productId; + + /** 下单时保存的商品名称,历史未记录时可为空。 */ + private String productName; + + /** 完成退款扣减后的有效商品份数,最小为零。 */ + private Integer productCount; + + /** 完成退款扣减后的有效商品金额,单位为分,最小为零。 */ + private Long totalPrice; +} diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCanteenApptTotalExportServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCanteenApptTotalExportServiceImpl.java index 9badde15b..31123719a 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCanteenApptTotalExportServiceImpl.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCanteenApptTotalExportServiceImpl.java @@ -27,6 +27,7 @@ import com.bsd.pjcloud.common.core.util.R; import com.bsd.pjcloud.hard.api.constant.order.HardHourTypeEnum; 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.HardSaleSourceEnum; import com.bsd.pjcloud.hard.api.dto.order.HardCanteenUserApptQueryDto; import com.bsd.pjcloud.hard.api.dto.order.HardOrderDto; import com.bsd.pjcloud.hard.api.entity.order.HardCarEntity; @@ -36,6 +37,7 @@ import com.bsd.pjcloud.hard.api.vo.order.HardOrderVo; import com.bsd.pjcloud.hard.api.vo.order.excel.HardCanteenUserApptExcelVo; import com.bsd.pjcloud.hard.mapper.order.HardCarMapper; import com.bsd.pjcloud.hard.mapper.order.HardOrderMapper; +import com.bsd.pjcloud.hard.mapper.order.model.HardCanteenReservationSaleFact; import com.bsd.pjcloud.hard.service.HardCanteenApptTotalExportService; import com.bsd.pjcloud.wallet.api.feign.RemoteMchService; import com.bsd.pjcloud.wallet.api.vo.MchManageVo; @@ -499,15 +501,15 @@ public class HardCanteenApptTotalExportServiceImpl implements HardCanteenApptTot private void validateQuery(HardCanteenUserApptQueryDto query) { if (query == null || query.getApptUseDateList() == null || query.getApptUseDateList().length != 2 || query.getApptUseDateList()[0] == null || query.getApptUseDateList()[1] == null) { - throw new IllegalArgumentException("用餐日期范围必须包含开始和结束日期"); + throw new IllegalArgumentException("统计日期范围必须包含开始和结束日期"); } LocalDate startDate = query.getApptUseDateList()[0]; LocalDate endDate = query.getApptUseDateList()[1]; if (endDate.isBefore(startDate)) { - throw new IllegalArgumentException("用餐结束日期不能早于开始日期"); + throw new IllegalArgumentException("统计结束日期不能早于开始日期"); } if (endDate.isAfter(startDate.plusMonths(MAX_QUERY_MONTHS))) { - throw new IllegalArgumentException("用餐日期范围最多查询两个月"); + throw new IllegalArgumentException("统计日期范围最多查询两个月"); } if (StrUtil.isNotBlank(query.getReservationStatus()) && !RESERVATION_STATUS_VALUES.contains(query.getReservationStatus())) { @@ -520,7 +522,7 @@ public class HardCanteenApptTotalExportServiceImpl implements HardCanteenApptTot } /** - * 展开查询范围内的全部自然日,开始和结束日期均包含在结果中。 + * 展开统计范围内的全部自然日,开始和结束日期均包含在结果中。 * * @param query 已通过日期范围校验的查询条件 * @return 按日期升序排列的查询日期 @@ -697,9 +699,9 @@ public class HardCanteenApptTotalExportServiceImpl implements HardCanteenApptTot } Map merchantNameMap = fetchMerchantNameMap(orders); Map>> orderMap = orders.stream() - .filter(order -> order.getUserId() != null && order.getApptUseDate() != null) + .filter(order -> order.getUserId() != null && resolveStatisticsDate(order, query) != null) .collect(Collectors.groupingBy(HardOrderVo::getUserId, LinkedHashMap::new, - Collectors.groupingBy(HardOrderVo::getApptUseDate, LinkedHashMap::new, + Collectors.groupingBy(order -> resolveStatisticsDate(order, query), LinkedHashMap::new, Collectors.mapping(order -> toOrderVo(order, merchantNameMap), Collectors.toList())))); return users.stream().filter(user -> user.getUserId() != null).flatMap(user -> queryDates.stream().map(date -> { @@ -754,16 +756,54 @@ public class HardCanteenApptTotalExportServiceImpl implements HardCanteenApptTot new ArrayList<>(userIds.subList(start, end))); orders.addAll(hardOrderMapper.scopeList(orderQuery, null)); } - return orders; + return retainEffectiveReservationOrders(orders, query); } /** - * 分批读取预约订单关联的购物车快照,避免统计请求因用户范围过大而生成超长 IN 条件。 + * 只保留完成退款后仍有有效商品的预约订单,并把导出金额改为退款后的净商品金额。 + * + * @param orders 已按订单状态和业务日期查询出的预约订单 + * @param query 当前导出筛选条件 + * @return 完成退款后仍有商品的订单,订单总价已更新为净额 + */ + private List retainEffectiveReservationOrders(List orders, + HardCanteenUserApptQueryDto query) { + if (orders.isEmpty()) { + return List.of(); + } + Map effectiveTotalPriceMap; + if (isVerifiedFilter(query)) { + Set orderIds = orders.stream().map(HardOrderVo::getId) + .filter(Objects::nonNull).collect(Collectors.toSet()); + effectiveTotalPriceMap = fetchReservationVerifiedSaleFacts( + orders.stream().map(HardOrderVo::getUserId).filter(Objects::nonNull).distinct().toList(), query) + .stream().filter(fact -> fact.getOrderId() != null && orderIds.contains(fact.getOrderId())) + .collect(Collectors.groupingBy(HardCanteenReservationSaleFact::getOrderId, + Collectors.summingLong(fact -> number(fact.getTotalPrice())))); + } else { + Map> carMap = fetchEffectiveReservationCars(orders); + effectiveTotalPriceMap = orders.stream() + .filter(order -> order.getId() != null && order.getParentCarId() != null) + .filter(order -> carMap.containsKey(order.getParentCarId())) + .collect(Collectors.toMap(HardOrderVo::getId, + order -> carMap.get(order.getParentCarId()).stream() + .map(HardCarEntity::getTotalPrice) + .mapToLong(HardCanteenApptTotalExportServiceImpl::number).sum(), + (left, right) -> left, LinkedHashMap::new)); + } + return orders.stream().filter(order -> order.getId() != null) + .filter(order -> effectiveTotalPriceMap.containsKey(order.getId())) + .peek(order -> order.setTotalPrice(Math.toIntExact(effectiveTotalPriceMap.get(order.getId())))) + .toList(); + } + + /** + * 分批读取预约订单关联的完成退款后有效商品,避免生成超长父购物车 IN 条件。 * * @param orders 当前筛选口径下的预约订单 - * @return 父购物车主键到商品快照的映射 + * @return 父购物车主键到退款后有效商品的映射 */ - private Map> fetchReservationCars(List orders) { + private Map> fetchEffectiveReservationCars(List orders) { List parentCarIds = orders.stream().map(HardOrderVo::getParentCarId) .filter(Objects::nonNull).distinct().toList(); if (parentCarIds.isEmpty()) { @@ -772,12 +812,41 @@ public class HardCanteenApptTotalExportServiceImpl implements HardCanteenApptTot Map> result = new LinkedHashMap<>(); for (int start = 0; start < parentCarIds.size(); start += ORDER_USER_BATCH_SIZE) { int end = Math.min(start + ORDER_USER_BATCH_SIZE, parentCarIds.size()); - hardCarMapper.listByParentCarIds(parentCarIds.subList(start, end)) + hardCarMapper.listEffectiveReservationCars(parentCarIds.subList(start, end)) .forEach(car -> result.computeIfAbsent(car.getParentCarId(), key -> new ArrayList<>()).add(car)); } return result; } + /** + * 按用户分批查询菜品销售统计中的预约核销商品事实。 + * + * @param userIds 已经过 UPMS 数据权限和人员条件限制的用户主键 + * @param query 核销日期、食堂和餐段筛选条件 + * @return 完成退款扣减后仍有商品的预约核销事实 + */ + private List fetchReservationVerifiedSaleFacts( + List userIds, HardCanteenUserApptQueryDto query) { + if (userIds.isEmpty()) { + return List.of(); + } + List facts = new ArrayList<>(); + for (int start = 0; start < userIds.size(); start += ORDER_USER_BATCH_SIZE) { + int end = Math.min(start + ORDER_USER_BATCH_SIZE, userIds.size()); + HardOrderDto factQuery = new HardOrderDto(); + factQuery.setUserIds(new ArrayList<>(userIds.subList(start, end))); + factQuery.setMerchantId(query.getMerchantId()); + factQuery.setHourType(query.getHourType()); + factQuery.setSaleSourceCodes(List.of(HardSaleSourceEnum.RESERVATION_VERIFIED.getCode())); + factQuery.setStatisticsDateTimeList(new LocalDateTime[]{ + query.getApptUseDateList()[0].atStartOfDay(), + query.getApptUseDateList()[1].atTime(LocalTime.MAX) + }); + facts.addAll(hardCarMapper.listReservationVerifiedSaleFacts(factQuery)); + } + return facts; + } + /** * 构造用户预约列表、候选用户预查询和导出明细共用的订单条件。 * @@ -795,15 +864,64 @@ public class HardCanteenApptTotalExportServiceImpl implements HardCanteenApptTot orderQuery.setMerchantId(query.getMerchantId()); orderQuery.setHourType(query.getHourType()); orderQuery.setOrderType(HardOrderTypeEnum.RESERVE.getType()); - orderQuery.setOrderStatusCodes(VALID_RESERVATION_ORDER_STATUSES); orderQuery.setApptIdOrApptRuleIdNotNull(true); - orderQuery.setApptUseDateTimeList(new java.time.LocalDateTime[]{ - query.getApptUseDateList()[0].atStartOfDay(), - query.getApptUseDateList()[1].atTime(LocalTime.MAX) - }); + LocalDateTime[] dateTimeRange = new LocalDateTime[]{ + query.getApptUseDateList()[0].atStartOfDay(), query.getApptUseDateList()[1].atTime(LocalTime.MAX)}; + if (isVerifiedFilter(query)) { + orderQuery.setOrderStatusCodes(List.of(HardOrderStatusEnum.CANCEL.getType())); + orderQuery.setVerificationTimeList(dateTimeRange); + } else { + orderQuery.setOrderStatusCodes(isUnverifiedFilter(query) + ? List.of(HardOrderStatusEnum.PAY.getType()) : VALID_RESERVATION_ORDER_STATUSES); + orderQuery.setApptUseDateTimeList(dateTimeRange); + } return orderQuery; } + /** + * 解析导出行的业务统计日期。 + * + * @param order 预约订单 + * @param query 当前核销状态筛选 + * @return 已核销筛选返回实际核销日期,其他情况返回预约用餐日期 + */ + private LocalDate resolveStatisticsDate(HardOrderVo order, HardCanteenUserApptQueryDto query) { + if (isVerifiedFilter(query)) { + return order.getVerificationTime() == null ? null : order.getVerificationTime().toLocalDate(); + } + return order.getApptUseDate(); + } + + /** + * 判断当前导出是否要求按单笔已核销订单统计。 + * + * @param query 导出查询条件 + * @return 核销状态为 VERIFIED 时返回 true + */ + private boolean isVerifiedFilter(HardCanteenUserApptQueryDto query) { + return Objects.equals(query.getVerificationStatus(), VERIFICATION_STATUS_VERIFIED); + } + + /** + * 判断当前导出是否要求按单笔待核销订单统计。 + * + * @param query 导出查询条件 + * @return 核销状态为 UNVERIFIED 时返回 true + */ + private boolean isUnverifiedFilter(HardCanteenUserApptQueryDto query) { + return Objects.equals(query.getVerificationStatus(), VERIFICATION_STATUS_UNVERIFIED); + } + + /** + * 将可能为空的商品金额安全归一为长整型零值。 + * + * @param value 商品金额,单位为分 + * @return 原数值的长整型表示;空值返回零 + */ + private static long number(Number value) { + return value == null ? 0L : value.longValue(); + } + /** * 批量补齐订单关联的食堂名称;商户服务异常时保留订单并返回空名称,避免影响预约事实查询。 * @@ -843,6 +961,7 @@ public class HardCanteenApptTotalExportServiceImpl implements HardCanteenApptTot HardCanteenUserApptOrderVo detail = new HardCanteenUserApptOrderVo(); detail.setId(order.getId()); detail.setHourType(order.getHourType()); + detail.setApptUseDate(order.getApptUseDate()); detail.setProductDesc(order.getProductDesc()); detail.setMerchantName(merchantNameMap.get(order.getMerchantId())); detail.setTotalPrice(order.getTotalPrice()); diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCanteenApptTotalServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCanteenApptTotalServiceImpl.java index 99bb558c8..194e5e930 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCanteenApptTotalServiceImpl.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCanteenApptTotalServiceImpl.java @@ -13,6 +13,7 @@ import com.bsd.pjcloud.common.core.util.R; import com.bsd.pjcloud.common.data.datascope.DataScope; 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.HardSaleSourceEnum; import com.bsd.pjcloud.hard.api.dto.order.HardCanteenUserApptQueryDto; import com.bsd.pjcloud.hard.api.dto.order.HardOrderDto; import com.bsd.pjcloud.hard.api.entity.order.HardCarEntity; @@ -27,6 +28,7 @@ import com.bsd.pjcloud.hard.api.vo.order.HardCanteenUserApptStatisticsVo; import com.bsd.pjcloud.hard.api.vo.order.HardOrderVo; import com.bsd.pjcloud.hard.mapper.order.HardCarMapper; import com.bsd.pjcloud.hard.mapper.order.HardOrderMapper; +import com.bsd.pjcloud.hard.mapper.order.model.HardCanteenReservationSaleFact; import com.bsd.pjcloud.hard.service.HardCanteenApptTotalService; import com.bsd.pjcloud.wallet.api.feign.RemoteMchService; import com.bsd.pjcloud.wallet.api.vo.MchManageVo; @@ -260,8 +262,8 @@ public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalServ /** * 按当前用户预约查询条件生成每日和查询范围总计。 * - *

统计先复用用户预约列表的权限用户、有效订单和状态筛选口径,再读取订单购物车快照计算菜品份数, - * 保证抽屉中的人数、订单和菜品不会因为分页而少算或多算。

+ *

统计先复用用户预约列表的权限用户、有效订单和状态筛选口径,再读取完成退款扣减后的有效商品。 + * 已核销筛选直接复用菜品销售统计的核销事实,保证人数、订单、份数和金额不会因整行状态或分页产生偏差。

* * @param query 用户、身份、日期范围、食堂、餐段和状态筛选条件 * @return 按日期升序排列并包含范围总计的统计结果 @@ -276,7 +278,7 @@ public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalServ List rows = filterRows(buildRows(users, query, queryDates, orders), query); Map orderMap = orders.stream().filter(order -> order.getId() != null) .collect(Collectors.toMap(HardOrderVo::getId, Function.identity(), (left, right) -> left)); - Map> carMap = fetchReservationCars(orders); + Map> carMap = fetchReservationCars(orders, query); Map dailyBuckets = new LinkedHashMap<>(); queryDates.forEach(date -> dailyBuckets.put(date, new StatisticsBucket())); @@ -304,15 +306,15 @@ public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalServ private void validateQuery(HardCanteenUserApptQueryDto query) { if (query == null || query.getApptUseDateList() == null || query.getApptUseDateList().length != 2 || query.getApptUseDateList()[0] == null || query.getApptUseDateList()[1] == null) { - throw new IllegalArgumentException("用餐日期范围必须包含开始和结束日期"); + throw new IllegalArgumentException("统计日期范围必须包含开始和结束日期"); } LocalDate startDate = query.getApptUseDateList()[0]; LocalDate endDate = query.getApptUseDateList()[1]; if (endDate.isBefore(startDate)) { - throw new IllegalArgumentException("用餐结束日期不能早于开始日期"); + throw new IllegalArgumentException("统计结束日期不能早于开始日期"); } if (endDate.isAfter(startDate.plusMonths(MAX_QUERY_MONTHS))) { - throw new IllegalArgumentException("用餐日期范围最多查询两个月"); + throw new IllegalArgumentException("统计日期范围最多查询两个月"); } if (StrUtil.isNotBlank(query.getReservationStatus()) && !RESERVATION_STATUS_VALUES.contains(query.getReservationStatus())) { @@ -325,7 +327,7 @@ public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalServ } /** - * 展开查询范围内的全部自然日,开始和结束日期均包含在结果中。 + * 展开统计范围内的全部自然日,开始和结束日期均包含在结果中。 * * @param query 已通过日期范围校验的查询条件 * @return 按日期升序排列的查询日期 @@ -621,9 +623,9 @@ public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalServ } Map merchantNameMap = fetchMerchantNameMap(orders); Map>> orderMap = orders.stream() - .filter(order -> order.getUserId() != null && order.getApptUseDate() != null) + .filter(order -> order.getUserId() != null && resolveStatisticsDate(order, query) != null) .collect(Collectors.groupingBy(HardOrderVo::getUserId, LinkedHashMap::new, - Collectors.groupingBy(HardOrderVo::getApptUseDate, LinkedHashMap::new, + Collectors.groupingBy(order -> resolveStatisticsDate(order, query), LinkedHashMap::new, Collectors.mapping(order -> toOrderVo(order, merchantNameMap), Collectors.toList())))); return users.stream().filter(user -> user.getUserId() != null).flatMap(user -> queryDates.stream().map(date -> { @@ -678,16 +680,78 @@ public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalServ new ArrayList<>(userIds.subList(start, end))); orders.addAll(hardOrderMapper.scopeList(orderQuery, null)); } - return orders; + return retainEffectiveReservationOrders(orders, query); } /** - * 分批读取预约订单关联的购物车快照,避免统计请求因用户范围过大而生成超长 IN 条件。 + * 只保留完成退款后仍有有效商品的预约订单,并把订单展示金额改为退款后的净商品金额。 + * + *

已核销筛选直接使用菜品销售统计事实判定有效订单;其他预约状态读取退款后有效商品快照。 + * 这样全额退款订单不会继续计入人员、订单和金额,部分退款只保留实际剩余值。

+ * + * @param orders 已按订单状态和业务日期查询出的预约订单 + * @param query 当前人员页面筛选条件 + * @return 完成退款后仍有商品的订单,订单总价已更新为净额 + */ + private List retainEffectiveReservationOrders(List orders, + HardCanteenUserApptQueryDto query) { + if (orders.isEmpty()) { + return List.of(); + } + Map effectiveTotalPriceMap; + if (isVerifiedFilter(query)) { + Set orderIds = orders.stream().map(HardOrderVo::getId) + .filter(Objects::nonNull).collect(Collectors.toSet()); + effectiveTotalPriceMap = fetchReservationVerifiedSaleFacts( + orders.stream().map(HardOrderVo::getUserId).filter(Objects::nonNull).distinct().toList(), query) + .stream().filter(fact -> fact.getOrderId() != null && orderIds.contains(fact.getOrderId())) + .collect(Collectors.groupingBy(HardCanteenReservationSaleFact::getOrderId, + Collectors.summingLong(fact -> number(fact.getTotalPrice())))); + } else { + Map> carMap = fetchEffectiveReservationCars(orders); + effectiveTotalPriceMap = orders.stream() + .filter(order -> order.getId() != null && order.getParentCarId() != null) + .filter(order -> carMap.containsKey(order.getParentCarId())) + .collect(Collectors.toMap(HardOrderVo::getId, + order -> carMap.get(order.getParentCarId()).stream() + .map(HardCarEntity::getTotalPrice).mapToLong(HardCanteenApptTotalServiceImpl::number).sum(), + (left, right) -> left, LinkedHashMap::new)); + } + return orders.stream().filter(order -> order.getId() != null) + .filter(order -> effectiveTotalPriceMap.containsKey(order.getId())) + .peek(order -> order.setTotalPrice(Math.toIntExact(effectiveTotalPriceMap.get(order.getId())))) + .toList(); + } + + /** + * 分批读取预约订单关联的退款后有效商品,避免统计请求生成超长父购物车 IN 条件。 * * @param orders 当前筛选口径下的预约订单 - * @return 父购物车主键到商品快照的映射 + * @param query 当前人员页面筛选条件;已核销时用于切换到销售统计事实 + * @return 父购物车主键到有效商品快照的映射 */ - private Map> fetchReservationCars(List orders) { + private Map> fetchReservationCars(List orders, + HardCanteenUserApptQueryDto query) { + if (isVerifiedFilter(query)) { + Map orderMap = orders.stream().filter(order -> order.getId() != null) + .collect(Collectors.toMap(HardOrderVo::getId, Function.identity(), (left, right) -> left)); + return fetchReservationVerifiedSaleFacts( + orders.stream().map(HardOrderVo::getUserId).filter(Objects::nonNull).distinct().toList(), query) + .stream().filter(fact -> fact.getOrderId() != null && orderMap.containsKey(fact.getOrderId())) + .map(fact -> toEffectiveCar(fact, orderMap.get(fact.getOrderId()))) + .collect(Collectors.groupingBy(HardCarEntity::getParentCarId, LinkedHashMap::new, + Collectors.toList())); + } + return fetchEffectiveReservationCars(orders); + } + + /** + * 按父购物车分批读取完成退款扣减后的预约商品。 + * + * @param orders 当前筛选口径下的预约订单 + * @return 父购物车主键到退款后有效商品的映射 + */ + private Map> fetchEffectiveReservationCars(List orders) { List parentCarIds = orders.stream().map(HardOrderVo::getParentCarId) .filter(Objects::nonNull).distinct().toList(); if (parentCarIds.isEmpty()) { @@ -696,12 +760,58 @@ public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalServ Map> result = new LinkedHashMap<>(); for (int start = 0; start < parentCarIds.size(); start += ORDER_USER_BATCH_SIZE) { int end = Math.min(start + ORDER_USER_BATCH_SIZE, parentCarIds.size()); - hardCarMapper.listByParentCarIds(parentCarIds.subList(start, end)) + hardCarMapper.listEffectiveReservationCars(parentCarIds.subList(start, end)) .forEach(car -> result.computeIfAbsent(car.getParentCarId(), key -> new ArrayList<>()).add(car)); } return result; } + /** + * 按用户分批查询菜品销售统计中的预约核销商品事实。 + * + * @param userIds 已经过 UPMS 数据权限和人员条件限制的用户主键 + * @param query 核销日期、食堂和餐段筛选条件 + * @return 完成退款扣减后仍有商品的预约核销事实 + */ + private List fetchReservationVerifiedSaleFacts( + List userIds, HardCanteenUserApptQueryDto query) { + if (userIds.isEmpty()) { + return List.of(); + } + List facts = new ArrayList<>(); + for (int start = 0; start < userIds.size(); start += ORDER_USER_BATCH_SIZE) { + int end = Math.min(start + ORDER_USER_BATCH_SIZE, userIds.size()); + HardOrderDto factQuery = new HardOrderDto(); + factQuery.setUserIds(new ArrayList<>(userIds.subList(start, end))); + factQuery.setMerchantId(query.getMerchantId()); + factQuery.setHourType(query.getHourType()); + factQuery.setSaleSourceCodes(List.of(HardSaleSourceEnum.RESERVATION_VERIFIED.getCode())); + factQuery.setStatisticsDateTimeList(new java.time.LocalDateTime[]{ + query.getApptUseDateList()[0].atStartOfDay(), + query.getApptUseDateList()[1].atTime(LocalTime.MAX) + }); + facts.addAll(hardCarMapper.listReservationVerifiedSaleFacts(factQuery)); + } + return facts; + } + + /** + * 将销售统计商品事实转换为人员统计聚合器使用的有效购物车快照。 + * + * @param fact 已扣完成退款的预约核销商品事实 + * @param order 商品所属预约订单 + * @return 仅包含统计所需父购物车、商品、份数和金额的内存对象 + */ + private HardCarEntity toEffectiveCar(HardCanteenReservationSaleFact fact, HardOrderVo order) { + HardCarEntity car = new HardCarEntity(); + car.setParentCarId(order.getParentCarId()); + car.setProductId(fact.getProductId()); + car.setProductName(fact.getProductName()); + car.setProductCount(fact.getProductCount()); + car.setTotalPrice(fact.getTotalPrice()); + return car; + } + /** * 构造用户预约列表、候选用户预查询和导出明细共用的订单条件。 * @@ -719,15 +829,64 @@ public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalServ orderQuery.setMerchantId(query.getMerchantId()); orderQuery.setHourType(query.getHourType()); orderQuery.setOrderType(HardOrderTypeEnum.RESERVE.getType()); - orderQuery.setOrderStatusCodes(VALID_RESERVATION_ORDER_STATUSES); orderQuery.setApptIdOrApptRuleIdNotNull(true); - orderQuery.setApptUseDateTimeList(new java.time.LocalDateTime[]{ - query.getApptUseDateList()[0].atStartOfDay(), - query.getApptUseDateList()[1].atTime(LocalTime.MAX) - }); + java.time.LocalDateTime[] dateTimeRange = new java.time.LocalDateTime[]{ + query.getApptUseDateList()[0].atStartOfDay(), query.getApptUseDateList()[1].atTime(LocalTime.MAX)}; + if (isVerifiedFilter(query)) { + orderQuery.setOrderStatusCodes(List.of(HardOrderStatusEnum.CANCEL.getType())); + orderQuery.setVerificationTimeList(dateTimeRange); + } else { + orderQuery.setOrderStatusCodes(isUnverifiedFilter(query) + ? List.of(HardOrderStatusEnum.PAY.getType()) : VALID_RESERVATION_ORDER_STATUSES); + orderQuery.setApptUseDateTimeList(dateTimeRange); + } return orderQuery; } + /** + * 解析当前订单在人员页面中的统计日期。 + * + * @param order 预约订单 + * @param query 当前核销状态筛选 + * @return 已核销筛选返回实际核销日期,其他情况返回预约用餐日期 + */ + private LocalDate resolveStatisticsDate(HardOrderVo order, HardCanteenUserApptQueryDto query) { + if (isVerifiedFilter(query)) { + return order.getVerificationTime() == null ? null : order.getVerificationTime().toLocalDate(); + } + return order.getApptUseDate(); + } + + /** + * 判断当前查询是否要求按单笔已核销订单统计。 + * + * @param query 人员页面查询条件 + * @return 核销状态为 VERIFIED 时返回 true + */ + private boolean isVerifiedFilter(HardCanteenUserApptQueryDto query) { + return Objects.equals(query.getVerificationStatus(), VERIFICATION_STATUS_VERIFIED); + } + + /** + * 判断当前查询是否要求按单笔待核销订单统计。 + * + * @param query 人员页面查询条件 + * @return 核销状态为 UNVERIFIED 时返回 true + */ + private boolean isUnverifiedFilter(HardCanteenUserApptQueryDto query) { + return Objects.equals(query.getVerificationStatus(), VERIFICATION_STATUS_UNVERIFIED); + } + + /** + * 将可能为空的数值安全归一为长整型零值。 + * + * @param value 商品数量或金额 + * @return 原数值的长整型表示;空值返回零 + */ + private static long number(Number value) { + return value == null ? 0L : value.longValue(); + } + /** * 批量补齐订单关联的食堂名称;商户服务异常时保留订单并返回空名称,避免影响预约事实查询。 * @@ -767,6 +926,7 @@ public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalServ HardCanteenUserApptOrderVo detail = new HardCanteenUserApptOrderVo(); detail.setId(order.getId()); detail.setHourType(order.getHourType()); + detail.setApptUseDate(order.getApptUseDate()); detail.setProductDesc(order.getProductDesc()); detail.setMerchantName(merchantNameMap.get(order.getMerchantId())); detail.setTotalPrice(order.getTotalPrice()); diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardCarMapper.xml b/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardCarMapper.xml index f080f7227..6c4483db2 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardCarMapper.xml +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardCarMapper.xml @@ -356,6 +356,44 @@ + + + + + +