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 efc66a9b8..3c507f54c 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 @@ -16,6 +16,7 @@ import com.bsd.pjcloud.hard.api.entity.manager.HardManagerEntity; import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity; import com.bsd.pjcloud.hard.api.vo.hard.PanelPorductVo; import com.bsd.pjcloud.hard.api.vo.order.*; +import com.bsd.pjcloud.hard.mapper.order.model.HardOrderDepartmentStatisticsDetail; import com.github.yulichang.wrapper.MPJLambdaWrapper; import org.apache.ibatis.annotations.Mapper; @@ -169,6 +170,14 @@ public interface HardOrderMapper extends PjcloudBaseMapper { List refundSumAndCount(@Param("dto") HardOrderSumAndCountDto refundDto); + /** + * 查询部门交易统计的逐笔订单支付明细;钱包支付会按流水历史部门重新归集,SQL 不做部门过滤。 + * + * @param dto 订单状态、商户、时间和钱包类型筛选条件 + * @return 逐笔支付明细,bizId 为订单 ID 字符串 + */ + List listPayStatisticsDetails(@Param("dto") HardOrderSumAndCountDto dto); + List sumAndCountTotalByHourType(@Param("dto") HardOrderSumAndCountDto dto); HardZyCardSyncVo totalYearMonthDay(@Param("dto") HardOrderDto hardOrderDto); diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/model/HardOrderDepartmentStatisticsDetail.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/model/HardOrderDepartmentStatisticsDetail.java new file mode 100644 index 000000000..38d1f815a --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/model/HardOrderDepartmentStatisticsDetail.java @@ -0,0 +1,25 @@ +package com.bsd.pjcloud.hard.mapper.order.model; + +import lombok.Data; + +/** + * 部门交易统计使用的逐笔订单或售后明细,仅在 Hardware 服务内部参与历史部门重新归集。 + */ +@Data +public class HardOrderDepartmentStatisticsDetail { + + /** 钱包流水使用的稳定业务号;支付对应订单 ID,退款对应售后单 ID。 */ + private String bizId; + + /** 原订单绑定的钱包 ID;非钱包交易和部分历史异常订单允许为空。 */ + private Long walletId; + + /** 原订单创建时保存的部门 ID,在没有对应钱包流水时作为历史归属回退值。 */ + private Long deptId; + + /** 单笔支付或退款金额,单位分;历史退款金额为空时由 SQL 转为零。 */ + private Long sum; + + /** 当前明细对应的统计次数,支付订单和售后记录均固定为一。 */ + private Long count; +} diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderTotalServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderTotalServiceImpl.java index 42549e35a..c615e9519 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderTotalServiceImpl.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderTotalServiceImpl.java @@ -19,6 +19,7 @@ import com.bsd.pjcloud.admin.api.entity.SysDept; import com.bsd.pjcloud.admin.api.feign.RemoteDeptService; import com.bsd.pjcloud.common.core.util.BeanUtils; import com.bsd.pjcloud.common.core.util.CollectionUtils; +import com.bsd.pjcloud.common.core.util.R; import com.bsd.pjcloud.common.data.datascope.DataScope; import com.bsd.pjcloud.common.data.datascope.DataScopeFuncEnum; import com.bsd.pjcloud.common.security.service.PjcloudUser; @@ -35,15 +36,19 @@ import com.bsd.pjcloud.hard.api.utils.HardUtil; import com.bsd.pjcloud.hard.api.vo.order.*; import com.bsd.pjcloud.hard.mapper.manager.HardManagerMapper; import com.bsd.pjcloud.hard.mapper.order.HardOrderMapper; +import com.bsd.pjcloud.hard.mapper.order.model.HardOrderDepartmentStatisticsDetail; import com.bsd.pjcloud.hard.service.HardManagerService; import com.bsd.pjcloud.hard.service.HardOrderTotalService; import com.bsd.pjcloud.hard.utils.RemoteBatchQueryUtil; import com.bsd.pjcloud.wallet.api.constant.WalletBizEnum; +import com.bsd.pjcloud.wallet.api.dto.WalletTransactionDepartmentSnapshotBatchDto; +import com.bsd.pjcloud.wallet.api.dto.WalletTransactionDepartmentSnapshotDto; import com.bsd.pjcloud.wallet.api.dto.WalletTransactionSumAndCountDto; import com.bsd.pjcloud.wallet.api.feign.RemoteMchService; import com.bsd.pjcloud.wallet.api.feign.RemoteWalletTransactionService; import com.bsd.pjcloud.wallet.api.utils.WalletDataUtil; import com.bsd.pjcloud.wallet.api.vo.MchManageVo; +import com.bsd.pjcloud.wallet.api.vo.WalletTransactionDepartmentSnapshotVo; import com.bsd.pjcloud.wallet.api.vo.WalletTransactionSumAndCountVo; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -62,6 +67,7 @@ import java.time.*; import java.time.temporal.TemporalAdjusters; import java.util.*; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; @Validated @Slf4j @@ -220,7 +226,7 @@ public class HardOrderTotalServiceImpl implements HardOrderTotalService { } else { dto.setMchTypeCodes(dto.getMchTypeCodes()); } - // 查询总流水 + // 流水与退款的业务发生时间不同,必须分别查询后再按商户合并,避免直接关联造成一笔订单被多笔售后重复放大。 HardOrderDto totalDto = BeanUtil.toBean(dto, HardOrderDto.class); totalDto.setOrderStatusCodes(HardOrderStatusEnum.getPayPrice()); DataScope totalDataScope = DataScope.of(); @@ -230,7 +236,9 @@ public class HardOrderTotalServiceImpl implements HardOrderTotalService { HardOrderDto refundDto = BeanUtil.toBean(dto, HardOrderDto.class); refundDto.setPayTimeList(null); refundDto.setRefundTimeList(dto.getPayTimeList()); - refundDto.setOrderStatusCodes(HardOrderStatusEnum.getPayErrorCodes()); + // refundStatusCodes 属于订单退款状态,售后完成状态必须使用 saleStatusCodes,二者混用会把有效退款全部过滤掉。 + // 部分退款后订单仍是已支付状态,退款统计不能只查询整单退款状态的订单。 + refundDto.setOrderStatusCodes(HardOrderStatusEnum.getPayPrice()); refundDto.setRefundStatusCodes(List.of(HardSaleStatusEnum.COMPLETE.getStatus())); DataScope refundDataScope = DataScope.of(); refundDataScope.setScopeDeptName("merchant_dept_id"); @@ -327,7 +335,8 @@ public class HardOrderTotalServiceImpl implements HardOrderTotalService { refundDto.setRefundTimeList(dto.getPayTimeList()); refundDto.setAuditTimeYearStr(dto.getPayTimeYearStr()); refundDto.setPayTimeYearStr(null); - refundDto.setOrderStatusCodes(HardOrderStatusEnum.getPayErrorCodes()); + // 部分退款后订单仍是已支付状态,退款统计不能只查询整单退款状态的订单;审核中的售后单未完成退款,不计入统计。 + refundDto.setOrderStatusCodes(HardOrderStatusEnum.getPayPrice()); refundDto.setRefundStatusCodes(List.of(HardSaleStatusEnum.COMPLETE.getStatus())); if (ObjectUtil.isNotEmpty(dto.getMchIds())) { refundDto.setMchIds(dto.getMchIds()); @@ -514,10 +523,99 @@ public class HardOrderTotalServiceImpl implements HardOrderTotalService { /** * 部门交易记录 + * + *

钱包操作与退款金额直接按 wallet_transaction 流水汇总;订单支付先取逐笔明细, + * 再按支付流水创建时固化的部门重新归集,现金、第三方支付回退订单部门快照。 + * 退款金额保留 REFUND 流水原始正负号,审核中的退款尚未生成流水,不会计入任何金额。

+ * + * @param dto 当前用户权限范围内的部门、时间和钱包类型筛选条件 + * @return 部门交易统计;筛选范围内没有部门时返回空列表 + * @throws IllegalStateException 部门或钱包远程统计调用失败时抛出,避免返回不完整报表 */ @Override public List pageListDept(HardDeptTotalDto dto) { - // 查询部门 + List deptList = queryDepartmentList(dto); + if (ObjectUtil.isEmpty(deptList)) { + return List.of(); + } + + List walletBatchList = queryWalletStatistics( + buildWalletStatisticsDto(dto, List.of(WalletBizEnum.BATCH.getCode()), WalletTransactionSumAndCountDto.TYPE_INCOME)); + List walletReduceList = queryWalletStatistics( + buildWalletStatisticsDto(dto, List.of(WalletBizEnum.BATCH.getCode()), WalletTransactionSumAndCountDto.TYPE_EXPENSE)); + List walletReturnCanteenList = queryWalletStatistics( + buildWalletStatisticsDto(dto, List.of(WalletBizEnum.RETURN_CANTEEN.getCode()), null)); + List walletOnceBatchList = queryWalletStatistics( + buildWalletStatisticsDto(dto, List.of(WalletBizEnum.UPDATE.getCode(), WalletBizEnum.RECHARGE.getCode()), WalletTransactionSumAndCountDto.TYPE_INCOME)); + List walletOnceReduceList = queryWalletStatistics( + buildWalletStatisticsDto(dto, List.of(WalletBizEnum.UPDATE.getCode(), WalletBizEnum.RECHARGE.getCode()), WalletTransactionSumAndCountDto.TYPE_EXPENSE)); + List walletRefundList = queryWalletStatistics( + buildWalletStatisticsDto(dto, List.of(WalletBizEnum.REFUND.getCode()), null)); + + List payDetails = + hardOrderMapper.listPayStatisticsDetails(buildPayStatisticsDto(dto)); + Map> detailsByBizType = new LinkedHashMap<>(); + detailsByBizType.put(WalletBizEnum.PAYMENT.getCode(), payDetails); + Map> transactionSnapshotMaps = + queryTransactionDepartmentSnapshotMaps(detailsByBizType); + List payList = aggregatePaymentsByTransactionDepartment(payDetails, + transactionSnapshotMaps.getOrDefault(WalletBizEnum.PAYMENT.getCode(), Map.of()), dto); + + Map walletBatchMap = indexWalletStatistics(walletBatchList); + Map walletReduceMap = indexWalletStatistics(walletReduceList); + Map walletReturnCanteenMap = indexWalletStatistics(walletReturnCanteenList); + Map walletOnceBatchMap = indexWalletStatistics(walletOnceBatchList); + Map walletOnceReduceMap = indexWalletStatistics(walletOnceReduceList); + Map walletRefundMap = indexWalletStatistics(walletRefundList); + Map payMap = indexOrderStatistics(payList); + + List resultList = new ArrayList<>(deptList.size()); + for (SysDept dept : deptList) { + HardDeptTotalVo statistics = new HardDeptTotalVo(); + statistics.setDeptId(dept.getDeptId()); + statistics.setDeptName(dept.getName()); + + WalletTransactionSumAndCountVo walletBatch = walletBatchMap.get(dept.getDeptId()); + if (walletBatch != null) { + statistics.setWalletPrice(BigDecimal.valueOf(ObjectUtil.defaultIfNull(walletBatch.getSum(), 0L))); + statistics.setWalletCount(ObjectUtil.defaultIfNull(walletBatch.getCount(), 0L)); + } + WalletTransactionSumAndCountVo walletReduce = walletReduceMap.get(dept.getDeptId()); + if (walletReduce != null) { + statistics.setWalletReducePrice(Math.abs(ObjectUtil.defaultIfNull(walletReduce.getSum(), 0L))); + statistics.setWalletReduceCount(ObjectUtil.defaultIfNull(walletReduce.getCount(), 0L)); + } + WalletTransactionSumAndCountVo walletReturnCanteen = walletReturnCanteenMap.get(dept.getDeptId()); + if (walletReturnCanteen != null) { + statistics.setWalletReturnCanteenPrice(BigDecimal.valueOf(Math.abs( + ObjectUtil.defaultIfNull(walletReturnCanteen.getSum(), 0L)))); + statistics.setWalletReturnCanteenCount(ObjectUtil.defaultIfNull(walletReturnCanteen.getCount(), 0L)); + } + WalletTransactionSumAndCountVo walletOnceBatch = walletOnceBatchMap.get(dept.getDeptId()); + if (walletOnceBatch != null) { + statistics.setWalletOncePrice(BigDecimal.valueOf(ObjectUtil.defaultIfNull(walletOnceBatch.getSum(), 0L))); + statistics.setWalletOnceCount(ObjectUtil.defaultIfNull(walletOnceBatch.getCount(), 0L)); + } + WalletTransactionSumAndCountVo walletOnceReduce = walletOnceReduceMap.get(dept.getDeptId()); + if (walletOnceReduce != null) { + statistics.setWalletOnceReducePrice(BigDecimal.valueOf(Math.abs( + ObjectUtil.defaultIfNull(walletOnceReduce.getSum(), 0L)))); + statistics.setWalletOnceReduceCount(ObjectUtil.defaultIfNull(walletOnceReduce.getCount(), 0L)); + } + applyOrderStatistics(statistics, payMap.get(dept.getDeptId()), walletRefundMap.get(dept.getDeptId())); + resultList.add(statistics); + } + return resultList; + } + + /** + * 查询统计结果需要展示的部门,并校验 UPMS 远程调用结果。 + * + * @param dto 部门包含或排除筛选条件 + * @return 匹配的部门列表,远程响应数据为空时返回空列表 + * @throws IllegalStateException UPMS 部门远程调用失败时抛出 + */ + private List queryDepartmentList(HardDeptTotalDto dto) { DeptAllListDto deptDto = new DeptAllListDto(); deptDto.setIsInclude(dto.getIsInclude()); if (ObjectUtil.isNotEmpty(dto.getDeptId())) { @@ -527,193 +625,264 @@ public class HardOrderTotalServiceImpl implements HardOrderTotalService { deptDto.setDeptId(null); deptDto.setDeptIds(dto.getDeptIds()); } - List deptList = remoteDeptService.getInnerAllDept(deptDto).getData(); - if (ObjectUtil.isEmpty(deptList)) return null; - // List deptIds = deptList.stream().map(SysDept::getDeptId).filter(ObjectUtil::isNotEmpty).distinct().toList(); - List resultList = deptList.stream().map(v -> { - HardDeptTotalVo item = new HardDeptTotalVo(); - item.setDeptId(v.getDeptId()); - item.setDeptName(v.getName()); - return item; - }).toList(); - // 查询钱包批量充值流水 - CompletableFuture> walletBatchFuture = CompletableFuture.supplyAsync(() -> { - WalletTransactionSumAndCountDto walletDto = new WalletTransactionSumAndCountDto(); - walletDto.setDeptIds(dto.getDeptIds()); - walletDto.setIsInclude(dto.getIsInclude()); - walletDto.setWalletTypeIdList(dto.getWalletTypeIdList()); - walletDto.setBizTypes(List.of(WalletBizEnum.BATCH.getCode())); - walletDto.setType(WalletTransactionSumAndCountDto.TYPE_INCOME); - if (ObjectUtil.isNotEmpty(dto.getCreateTimeList())) { - walletDto.setCreateTimeList(dto.getCreateTimeList()); + R> response = remoteDeptService.getInnerAllDept(deptDto); + if (response == null || !response.isOk()) { + throw new IllegalStateException("查询部门交易统计的部门信息失败"); + } + return response.getData() == null ? List.of() : response.getData(); + } + + /** + * 构建钱包部门统计条件,保证六类钱包统计使用完全一致的权限、时间和钱包类型范围。 + * + * @param dto 部门统计页面筛选条件 + * @param bizTypes 钱包流水业务类型编码集合 + * @param type 收支方向;为空时不限制流水正负方向 + * @return 钱包流水汇总条件 + */ + private WalletTransactionSumAndCountDto buildWalletStatisticsDto(HardDeptTotalDto dto, List bizTypes, Integer type) { + WalletTransactionSumAndCountDto walletDto = new WalletTransactionSumAndCountDto(); + walletDto.setDeptIds(dto.getDeptIds()); + walletDto.setIsInclude(dto.getIsInclude()); + walletDto.setBizTypes(bizTypes); + walletDto.setType(type); + walletDto.setCreateTimeList(dto.getCreateTimeList()); + walletDto.setWalletTypeId(dto.getWalletTypeId()); + walletDto.setWalletTypeIdList(dto.getWalletTypeIdList()); + return walletDto; + } + + /** + * 调用钱包服务查询部门流水统计;远程业务失败时终止本次汇总,避免把失败数据误展示为零。 + * + * @param walletDto 钱包流水汇总条件 + * @return 钱包部门统计,远程响应数据为空时返回空列表 + * @throws IllegalStateException 钱包流水远程调用失败时抛出 + */ + private List queryWalletStatistics(WalletTransactionSumAndCountDto walletDto) { + R> response = remoteWalletTransactionService.sumAndCount(walletDto); + if (response == null || !response.isOk()) { + throw new IllegalStateException("查询钱包部门交易统计失败"); + } + return response.getData() == null ? List.of() : response.getData(); + } + + /** + * 构建硬件订单支付部门统计条件。订单金额仍保留非钱包支付能力,钱包支付会在后续按流水历史部门重新归属; + * 明细查询不做部门过滤,部门包含/排除条件在重归集后统一应用。 + * + * @param dto 部门统计页面筛选条件 + * @return 支付明细查询条件 + */ + private HardOrderSumAndCountDto buildPayStatisticsDto(HardDeptTotalDto dto) { + HardOrderSumAndCountDto orderDto = new HardOrderSumAndCountDto(); + orderDto.setUserDeptIds(dto.getDeptIds()); + orderDto.setIsInclude(dto.getIsInclude()); + orderDto.setOrderStatusCodes(HardOrderStatusEnum.getPayPrice()); + orderDto.setWalletTypeId(dto.getWalletTypeId()); + orderDto.setWalletTypeIdList(dto.getWalletTypeIdList()); + orderDto.setCreateTimeList(dto.getCreateTimeList()); + return orderDto; + } + + /** + * 将订单支付明细按钱包流水发生时保存的部门重新归集,并在重新归属后应用页面部门条件。 + * + *

现金、第三方支付或历史异常记录没有对应钱包流水时,继续使用订单创建时保存的部门快照, + * 避免丢失原有非钱包交易能力。退款不再进入本方法,统一由 Wallet 按 REFUND 流水直接汇总。

+ * + * @param details 符合订单状态、商户和时间条件的支付明细 + * @param transactionSnapshotMap 以订单业务号为键的钱包支付流水快照 + * @param dto 页面部门包含或排除条件 + * @return 按最终历史部门归集后的金额和次数 + */ + private List aggregatePaymentsByTransactionDepartment( + List details, + Map transactionSnapshotMap, + HardDeptTotalDto dto) { + if (ObjectUtil.isEmpty(details)) { + return List.of(); + } + Set departmentFilter = ObjectUtil.isEmpty(dto.getDeptIds()) + ? Set.of() : new HashSet<>(dto.getDeptIds()); + Map aggregateMap = new LinkedHashMap<>(); + for (HardOrderDepartmentStatisticsDetail detail : details) { + if (detail == null) { + continue; } - if (ObjectUtil.isNotEmpty(dto.getWalletTypeId())) { - walletDto.setWalletTypeId(dto.getWalletTypeId()); + WalletTransactionDepartmentSnapshotVo snapshot = transactionSnapshotMap.get(detail.getBizId()); + Long departmentId = snapshot == null ? detail.getDeptId() : snapshot.getDeptId(); + if (departmentId == null || !matchesDepartmentFilter(departmentId, departmentFilter, dto.getIsInclude())) { + continue; } - return remoteWalletTransactionService.sumAndCount(walletDto).getData(); - }); - // 查询钱包批量扣减流水 - CompletableFuture> walletReduceFuture = CompletableFuture.supplyAsync(() -> { - WalletTransactionSumAndCountDto walletDto = new WalletTransactionSumAndCountDto(); - walletDto.setDeptIds(dto.getDeptIds()); - walletDto.setIsInclude(dto.getIsInclude()); - walletDto.setWalletTypeIdList(dto.getWalletTypeIdList()); - walletDto.setBizTypes(List.of(WalletBizEnum.BATCH.getCode())); - walletDto.setType(WalletTransactionSumAndCountDto.TYPE_EXPENSE); - if (ObjectUtil.isNotEmpty(dto.getCreateTimeList())) { - walletDto.setCreateTimeList(dto.getCreateTimeList()); - } - if (ObjectUtil.isNotEmpty(dto.getWalletTypeId())) { - walletDto.setWalletTypeId(dto.getWalletTypeId()); - } - return remoteWalletTransactionService.sumAndCount(walletDto).getData(); - }); - // 查询钱包退还各自食堂 - CompletableFuture> walletRetrunCanteenFuture = CompletableFuture.supplyAsync(() -> { - WalletTransactionSumAndCountDto walletDto = new WalletTransactionSumAndCountDto(); - walletDto.setDeptIds(dto.getDeptIds()); - walletDto.setIsInclude(dto.getIsInclude()); - walletDto.setWalletTypeIdList(dto.getWalletTypeIdList()); - walletDto.setBizTypes(List.of(WalletBizEnum.RETURN_CANTEEN.getCode())); - // walletDto.setType(WalletTransactionSumAndCountDto.TYPE_EXPENSE); - if (ObjectUtil.isNotEmpty(dto.getCreateTimeList())) { - walletDto.setCreateTimeList(dto.getCreateTimeList()); - } - if (ObjectUtil.isNotEmpty(dto.getWalletTypeId())) { - walletDto.setWalletTypeId(dto.getWalletTypeId()); - } - return remoteWalletTransactionService.sumAndCount(walletDto).getData(); - }); - // 查询钱包单次充值流水 - CompletableFuture> walletOnceBatchFuture = CompletableFuture.supplyAsync(() -> { - WalletTransactionSumAndCountDto walletDto = new WalletTransactionSumAndCountDto(); - walletDto.setDeptIds(dto.getDeptIds()); - walletDto.setIsInclude(dto.getIsInclude()); - walletDto.setWalletTypeIdList(dto.getWalletTypeIdList()); - walletDto.setBizTypes(List.of(WalletBizEnum.UPDATE.getCode(), WalletBizEnum.RECHARGE.getCode())); - walletDto.setType(WalletTransactionSumAndCountDto.TYPE_INCOME); - if (ObjectUtil.isNotEmpty(dto.getCreateTimeList())) { - walletDto.setCreateTimeList(dto.getCreateTimeList()); - } - if (ObjectUtil.isNotEmpty(dto.getWalletTypeId())) { - walletDto.setWalletTypeId(dto.getWalletTypeId()); - } - return remoteWalletTransactionService.sumAndCount(walletDto).getData(); - }); - // 查询钱包单次扣减流水 - CompletableFuture> walletOnceReduceFuture = CompletableFuture.supplyAsync(() -> { - WalletTransactionSumAndCountDto walletDto = new WalletTransactionSumAndCountDto(); - walletDto.setDeptIds(dto.getDeptIds()); - walletDto.setIsInclude(dto.getIsInclude()); - walletDto.setWalletTypeIdList(dto.getWalletTypeIdList()); - walletDto.setBizTypes(List.of(WalletBizEnum.UPDATE.getCode(), WalletBizEnum.RECHARGE.getCode())); - walletDto.setType(WalletTransactionSumAndCountDto.TYPE_EXPENSE); - if (ObjectUtil.isNotEmpty(dto.getCreateTimeList())) { - walletDto.setCreateTimeList(dto.getCreateTimeList()); - } - if (ObjectUtil.isNotEmpty(dto.getWalletTypeId())) { - walletDto.setWalletTypeId(dto.getWalletTypeId()); - } - return remoteWalletTransactionService.sumAndCount(walletDto).getData(); - }); - // 查询订单支付流水 - CompletableFuture> payFuture = CompletableFuture.supplyAsync(() -> { - HardOrderSumAndCountDto payDto = new HardOrderSumAndCountDto(); - payDto.setUserDeptIds(dto.getDeptIds()); - payDto.setIsInclude(dto.getIsInclude()); - payDto.setOrderStatusCodes(HardOrderStatusEnum.getPayPrice()); - payDto.setWalletTypeIdList(dto.getWalletTypeIdList()); - if (ObjectUtil.isNotEmpty(dto.getCreateTimeList())) { - payDto.setCreateTimeList(dto.getCreateTimeList()); - } - if (ObjectUtil.isNotEmpty(dto.getWalletTypeId())) { - payDto.setWalletTypeId(dto.getWalletTypeId()); - } - return hardOrderMapper.sumAndCount(payDto); - }); - // 查询订单退款流水 - CompletableFuture> refundFuture = CompletableFuture.supplyAsync(() -> { - HardOrderSumAndCountDto refundDto = new HardOrderSumAndCountDto(); - refundDto.setUserDeptIds(dto.getDeptIds()); - refundDto.setIsInclude(dto.getIsInclude()); - refundDto.setOrderStatusCodes(HardOrderStatusEnum.getPayErrorCodes()); - refundDto.setRefundStatusCodes(List.of(HardSaleStatusEnum.COMPLETE.getStatus())); - refundDto.setWalletTypeIdList(dto.getWalletTypeIdList()); - if (ObjectUtil.isNotEmpty(dto.getCreateTimeList())) { - refundDto.setRefundTimeList(dto.getCreateTimeList()); - } - if (ObjectUtil.isNotEmpty(dto.getWalletTypeId())) { - refundDto.setWalletTypeId(dto.getWalletTypeId()); - } - return hardOrderMapper.refundSumAndCount(refundDto); - }); - CompletableFuture allFutures = CompletableFuture.allOf(walletBatchFuture, walletReduceFuture, payFuture, refundFuture, walletOnceBatchFuture, walletOnceReduceFuture); - allFutures.join(); - List walletBatchList = walletBatchFuture.join(); - List walletReduceList = walletReduceFuture.join(); - List walletRetrunCanteenList = walletRetrunCanteenFuture.join(); - List walletOnceBatchList = walletOnceBatchFuture.join(); - List walletOnceReduceList = walletOnceReduceFuture.join(); - List payList = payFuture.join(); - List refundList = refundFuture.join(); - // 聚合一下数据 - for (HardDeptTotalVo v : resultList) { - if (ObjectUtil.isNotEmpty(walletBatchList)) { - walletBatchList.stream().filter(i -> ObjectUtil.equals(i.getDeptId(), v.getDeptId())).findFirst().ifPresent(i -> { - v.setWalletPrice(BigDecimal.valueOf(i.getSum())); - v.setWalletCount(i.getCount()); - }); - } - if (ObjectUtil.isNotEmpty(walletReduceList)) { - walletReduceList.stream().filter(i -> ObjectUtil.equals(i.getDeptId(), v.getDeptId())).findFirst().ifPresent(i -> { - v.setWalletReducePrice(Math.abs(i.getSum())); - v.setWalletReduceCount(i.getCount()); - }); - } - // 退还各自食堂 - if (ObjectUtil.isNotEmpty(walletRetrunCanteenList)) { - walletRetrunCanteenList.stream().filter(i -> ObjectUtil.equals(i.getDeptId(), v.getDeptId())).findFirst().ifPresent(i -> { - v.setWalletReturnCanteenPrice(BigDecimal.valueOf(Math.abs(i.getSum()))); - v.setWalletReturnCanteenCount(i.getCount()); - }); - } - if (ObjectUtil.isNotEmpty(walletOnceBatchList)) { - walletOnceBatchList.stream().filter(i -> ObjectUtil.equals(i.getDeptId(), v.getDeptId())).findFirst().ifPresent(i -> { - v.setWalletOncePrice(BigDecimal.valueOf(i.getSum())); - v.setWalletOnceCount(i.getCount()); - }); - } - if (ObjectUtil.isNotEmpty(walletOnceReduceList)) { - walletOnceReduceList.stream().filter(i -> ObjectUtil.equals(i.getDeptId(), v.getDeptId())).findFirst().ifPresent(i -> { - v.setWalletOnceReducePrice(BigDecimal.valueOf(Math.abs(i.getSum()))); - v.setWalletOnceReduceCount(i.getCount()); - }); - } - if (ObjectUtil.isNotEmpty(payList)) { - payList.stream().filter(i -> ObjectUtil.equals(i.getDeptId(), v.getDeptId())).findFirst().ifPresent(i -> { - v.setOrderPayPrice(BigDecimal.valueOf(i.getSum())); - v.setOrderPayCount(i.getCount()); - }); - } - if (ObjectUtil.isNotEmpty(refundList)) { - refundList.stream().filter(i -> ObjectUtil.equals(i.getDeptId(), v.getDeptId())).findFirst().ifPresent(i -> { - v.setOrderRefundPrice(BigDecimal.valueOf(i.getSum())); - v.setOrderRefundCont(i.getCount()); - }); + HardOrderSumAndCountVo aggregate = aggregateMap.computeIfAbsent(departmentId, key -> { + HardOrderSumAndCountVo value = new HardOrderSumAndCountVo(); + value.setDeptId(key); + value.setSum(0L); + value.setCount(0L); + return value; + }); + long amount = ObjectUtil.defaultIfNull(detail.getSum(), 0L); + aggregate.setSum(Math.addExact(aggregate.getSum(), amount)); + aggregate.setCount(Math.addExact(aggregate.getCount(), ObjectUtil.defaultIfNull(detail.getCount(), 0L))); + } + return new ArrayList<>(aggregateMap.values()); + } + + /** + * 将需要历史归属的交易明细整理成受控查询组,并用一次或极少数几次 Wallet 调用查询流水历史部门。 + * + *

单个 SQL 查询组最多一万条,单次远程请求最多十组。常规报表只产生一次远程调用, + * 极端大数据量会自动拆分远程请求,避免请求体或 SQL 参数失控。远程失败时终止统计, + * 禁止静默回退造成历史部门和订单部门混合口径。

+ * + * @param detailsByBizType 以钱包业务类型编码为键的待归属业务明细;部门页当前只传入支付订单 + * @return 第一层以业务类型编码为键、第二层以业务 ID 为键的钱包流水快照 + * @throws IllegalStateException 钱包历史部门查询失败时抛出 + */ + private Map> queryTransactionDepartmentSnapshotMaps( + Map> detailsByBizType) { + Map> expectedWalletMaps = new LinkedHashMap<>(); + Map> resultMaps = new LinkedHashMap<>(); + List queryGroups = new ArrayList<>(); + for (Map.Entry> entry : detailsByBizType.entrySet()) { + List sourceDetails = + entry.getValue() == null ? List.of() : entry.getValue(); + List walletDetails = sourceDetails.stream() + .filter(ObjectUtil::isNotEmpty) + .filter(detail -> ObjectUtil.isAllNotEmpty(detail.getBizId(), detail.getWalletId())) + .collect(Collectors.toMap(HardOrderDepartmentStatisticsDetail::getBizId, + detail -> detail, (first, ignored) -> first, LinkedHashMap::new)) + .values().stream() + .toList(); + Map expectedWalletMap = walletDetails.stream().collect(Collectors.toMap( + HardOrderDepartmentStatisticsDetail::getBizId, + HardOrderDepartmentStatisticsDetail::getWalletId, + (first, ignored) -> first, LinkedHashMap::new)); + expectedWalletMaps.put(entry.getKey(), expectedWalletMap); + resultMaps.put(entry.getKey(), new LinkedHashMap<>()); + for (int start = 0; start < walletDetails.size(); + start += WalletTransactionDepartmentSnapshotDto.MAX_QUERY_BATCH_SIZE) { + int end = Math.min(start + WalletTransactionDepartmentSnapshotDto.MAX_QUERY_BATCH_SIZE, + walletDetails.size()); + List batchDetails = walletDetails.subList(start, end); + WalletTransactionDepartmentSnapshotDto snapshotDto = new WalletTransactionDepartmentSnapshotDto(); + snapshotDto.setBizType(entry.getKey()); + snapshotDto.setBizIds(batchDetails.stream() + .map(HardOrderDepartmentStatisticsDetail::getBizId).toList()); + snapshotDto.setWalletIds(batchDetails.stream() + .map(HardOrderDepartmentStatisticsDetail::getWalletId).distinct().toList()); + queryGroups.add(snapshotDto); } } - // 计算净流水 - for (HardDeptTotalVo v : resultList) { - // 流水金额 - if (ObjectUtil.isNotEmpty(v.getOrderPayPrice())) { - v.setOrderProfitPrice(v.getOrderPayPrice().subtract(v.getOrderRefundPrice())); + for (int start = 0; start < queryGroups.size(); + start += WalletTransactionDepartmentSnapshotBatchDto.MAX_QUERY_GROUP_SIZE) { + int end = Math.min(start + WalletTransactionDepartmentSnapshotBatchDto.MAX_QUERY_GROUP_SIZE, + queryGroups.size()); + WalletTransactionDepartmentSnapshotBatchDto batchDto = + new WalletTransactionDepartmentSnapshotBatchDto(); + batchDto.setQueries(new ArrayList<>(queryGroups.subList(start, end))); + R> response = + remoteWalletTransactionService.listDepartmentSnapshotsBatch(batchDto); + if (response == null || !response.isOk()) { + throw new IllegalStateException("查询钱包流水历史部门失败"); } - // 流水次数 - if (ObjectUtil.isNotEmpty(v.getOrderPayCount())) { - v.setOrderProfitCont(v.getOrderPayCount() - v.getOrderRefundCont()); + if (ObjectUtil.isEmpty(response.getData())) { + continue; + } + for (WalletTransactionDepartmentSnapshotVo snapshot : response.getData()) { + if (snapshot != null && ObjectUtil.isAllNotEmpty( + snapshot.getBizType(), snapshot.getBizId(), snapshot.getWalletId(), snapshot.getDeptId())) { + Map expectedWalletMap = expectedWalletMaps.get(snapshot.getBizType()); + Map resultMap = resultMaps.get(snapshot.getBizType()); + if (expectedWalletMap == null || resultMap == null + || !ObjectUtil.equals(expectedWalletMap.get(snapshot.getBizId()), snapshot.getWalletId())) { + continue; + } + // 钱包服务按流水主键倒序返回,异常重复业务号保留最新一笔历史快照。 + resultMap.putIfAbsent(snapshot.getBizId(), snapshot); + } } } - return resultList; + return resultMaps; + } + + /** + * 判断重新归属后的历史部门是否命中页面包含或排除条件。 + * + * @param departmentId 流水历史部门或非钱包订单部门 + * @param departmentFilter 页面部门主键集合 + * @param isInclude true 表示包含,false 表示排除,空值兼容旧接口的包含语义 + * @return 当前历史部门是否参与统计 + */ + private boolean matchesDepartmentFilter(Long departmentId, Set departmentFilter, Boolean isInclude) { + if (departmentFilter.isEmpty()) { + return true; + } + boolean contains = departmentFilter.contains(departmentId); + return Boolean.FALSE.equals(isInclude) ? !contains : contains; + } + + /** + * 将钱包统计按部门建立索引,避免每个部门重复遍历全部统计结果。 + * + * @param statisticsList 钱包部门统计列表 + * @return 以部门 ID 为键的统计索引 + */ + private Map indexWalletStatistics(List statisticsList) { + Map statisticsMap = new LinkedHashMap<>(); + if (ObjectUtil.isNotEmpty(statisticsList)) { + for (WalletTransactionSumAndCountVo statistics : statisticsList) { + if (statistics != null && statistics.getDeptId() != null) { + statisticsMap.put(statistics.getDeptId(), statistics); + } + } + } + return statisticsMap; + } + + /** + * 将订单支付统计按部门建立索引,供支付结果与 Wallet 退款结果按相同键合并。 + * + * @param statisticsList 订单部门统计列表 + * @return 以部门 ID 为键的统计索引 + */ + private Map indexOrderStatistics(List statisticsList) { + Map statisticsMap = new LinkedHashMap<>(); + if (ObjectUtil.isNotEmpty(statisticsList)) { + for (HardOrderSumAndCountVo statistics : statisticsList) { + if (statistics != null && statistics.getDeptId() != null) { + statisticsMap.put(statistics.getDeptId(), statistics); + } + } + } + return statisticsMap; + } + + /** + * 合并部门的订单支付与 Wallet 退款流水,并计算净金额和净次数。 + * + *

退款金额保留 REFUND 流水原始正负号,退款次数按流水条数统计,因此退款到账、充值退款扣回 + * 与退款记录页面保持完全相同的流水口径;退款审核中的售后单尚未生成流水,不会计入。

+ * + * @param target 部门统计结果 + * @param pay 支付统计,允许为空 + * @param refund Wallet REFUND 流水统计,允许为空 + */ + private void applyOrderStatistics(HardDeptTotalVo target, HardOrderSumAndCountVo pay, + WalletTransactionSumAndCountVo refund) { + long payPrice = pay == null ? 0L : ObjectUtil.defaultIfNull(pay.getSum(), 0L); + long payCount = pay == null ? 0L : ObjectUtil.defaultIfNull(pay.getCount(), 0L); + long refundPrice = refund == null ? 0L : ObjectUtil.defaultIfNull(refund.getSum(), 0L); + long refundCount = refund == null ? 0L : ObjectUtil.defaultIfNull(refund.getCount(), 0L); + BigDecimal orderPayPrice = BigDecimal.valueOf(payPrice); + BigDecimal orderRefundPrice = BigDecimal.valueOf(refundPrice); + target.setOrderPayPrice(orderPayPrice); + target.setOrderPayCount(payCount); + target.setOrderRefundPrice(orderRefundPrice); + target.setOrderRefundCont(refundCount); + target.setOrderProfitPrice(orderPayPrice.subtract(orderRefundPrice)); + target.setOrderProfitCont(payCount - refundCount); } /** 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 153dcfaec..d0fbe0514 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 @@ -775,6 +775,50 @@ group by a.user_dept_id + + + + +