From cb74d0563fdc5694da7b0609c1bcdd437be7e2da Mon Sep 17 00:00:00 2001
From: dzc <2388224167@qq.com>
Date: Thu, 27 Aug 2026 21:51:19 +0800
Subject: [PATCH] 11
---
.../HardCanteenUserApptDishStatisticsVo.java | 4 +
.../order/HardOrderTotalController.java | 71 ++-
.../HardCanteenApptTotalServiceImpl.java | 8 +
.../impl/order/HardOrderTotalServiceImpl.java | 494 +++++++++---------
.../mapper/order/HardOrderMapper.xml | 5 +-
.../wallet/api/feign/RemoteMchService.java | 130 +++++
.../controller/mch/MchManageController.java | 16 +
.../wallet/service/MchManageService.java | 9 +
.../impl/mch/MchManageServiceImpl.java | 27 +
9 files changed, 527 insertions(+), 237 deletions(-)
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptDishStatisticsVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptDishStatisticsVo.java
index cff3371a7..581e8637d 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptDishStatisticsVo.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenUserApptDishStatisticsVo.java
@@ -21,6 +21,10 @@ public class HardCanteenUserApptDishStatisticsVo {
@Schema(description = "菜品名称")
private String productName;
+ /** 当前同餐段、同名菜品统计项实际涉及的商品 ID;同名商品可能因重复建档包含多个 ID,历史空值不写入集合。 */
+ @Schema(description = "商品ID集合")
+ private List productIds = new ArrayList<>();
+
/** 下单时固化的菜品图片地址;历史订单未保存图片时为空。 */
@Schema(description = "菜品图片地址")
private String productPic;
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderTotalController.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderTotalController.java
index db2cebc91..6451c2a53 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderTotalController.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderTotalController.java
@@ -47,8 +47,10 @@ import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
@RestController
@RequiredArgsConstructor
@@ -151,12 +153,70 @@ public class HardOrderTotalController {
return R.ok(hardOrderTotalService.listPayType(dto));
}
+ /**
+ * 查询当前用户所属食堂商户范围内的部门交易统计。
+ *
+ * @param dto 时间、钱包类型及部门树筛选条件;部门条件会与当前用户可管理的食堂部门取交集
+ * @return 当前用户有权查看的部门交易统计,无可见食堂或部门时返回空列表
+ */
@PostMapping("/page-list-dept")
@Operation(summary = "部门交易记录")
public R> pageListDept(@RequestBody HardDeptTotalDto dto) {
+ if (!applyCanteenDepartmentScope(dto)) {
+ return R.ok(List.of());
+ }
return R.ok(hardOrderTotalService.pageListDept(dto));
}
+ /**
+ * 将部门统计条件限制到当前用户可管理的食堂商户所属部门。
+ *
+ * 钱包服务根据当前用户主键和食堂商户类型一次性返回其所属食堂覆盖的用户部门。页面传入的
+ * 包含或排除条件只在该权限部门集合内生效,最终统一改写为明确的包含集合,避免空集合或排除
+ * 条件导致底层统计退化为全量查询。
+ *
+ * @param dto 待收敛的部门统计条件,方法会写入最终允许查询的部门 ID 集合
+ * @return 是否仍存在可查询部门;false 表示调用方必须直接返回空数据
+ */
+ private boolean applyCanteenDepartmentScope(HardDeptTotalDto dto) {
+ PjcloudUser user = SecurityUtils.getUser();
+ if (user == null || user.getId() == null) {
+ return false;
+ }
+
+ R> departmentResponse = mchService.getSelfMchUserDeptIdListByUserId(
+ user.getId(), List.of(MerchantTypeEnum.CANTEEN));
+ if (departmentResponse == null || !departmentResponse.isOk() || ObjectUtil.isEmpty(departmentResponse.getData())) {
+ return false;
+ }
+ List allowedDeptIds = departmentResponse.getData().stream()
+ .filter(id -> id != null && id > 0)
+ .distinct()
+ .toList();
+ if (ObjectUtil.isEmpty(allowedDeptIds)) {
+ return false;
+ }
+
+ List requestedDeptIds = ObjectUtil.isNotEmpty(dto.getDeptIds())
+ ? dto.getDeptIds()
+ : ObjectUtil.isNotEmpty(dto.getDeptId()) ? List.of(dto.getDeptId()) : List.of();
+ List scopedDeptIds = allowedDeptIds;
+ if (ObjectUtil.isNotEmpty(requestedDeptIds)) {
+ Set requestedDeptIdSet = new HashSet<>(requestedDeptIds);
+ scopedDeptIds = Boolean.FALSE.equals(dto.getIsInclude())
+ ? allowedDeptIds.stream().filter(id -> !requestedDeptIdSet.contains(id)).toList()
+ : allowedDeptIds.stream().filter(requestedDeptIdSet::contains).toList();
+ }
+ if (ObjectUtil.isEmpty(scopedDeptIds)) {
+ return false;
+ }
+
+ dto.setDeptId(null);
+ dto.setDeptIds(scopedDeptIds);
+ dto.setIsInclude(true);
+ return true;
+ }
+
@Operation(summary = "设备交易记录导出", description = "设备交易记录导出")
@PostMapping("/export-sb")
@SysLog("设备交易记录导出")
@@ -465,12 +525,21 @@ public class HardOrderTotalController {
}
}
+ /**
+ * 导出当前用户所属食堂商户范围内的部门交易统计。
+ *
+ * @param response Excel 文件响应
+ * @param dto 时间、钱包类型及部门树筛选条件;部门条件会与当前用户可管理的食堂部门取交集
+ * @throws IOException 写入 Excel 响应失败时抛出
+ */
@Operation(summary = "部门交易记录导出", description = "部门交易记录导出")
@PostMapping("/export-dept")
@SysLog("部门交易记录导出")
@PreAuthorize("@pms.hasPermission('hard_hardTotal_export_dept')")
public void exportDept(HttpServletResponse response, @RequestBody HardDeptTotalDto dto) throws IOException {
- List list = hardOrderTotalService.pageListDept(dto);
+ List list = applyCanteenDepartmentScope(dto)
+ ? hardOrderTotalService.pageListDept(dto)
+ : List.of();
try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).withTemplate(ResourceUtil.getStream("file/部门交易金额汇总.xlsx")).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet().build();
FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
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 f26b5763c..9bdd41165 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
@@ -43,6 +43,7 @@ import java.time.LocalTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -1122,6 +1123,9 @@ public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalServ
String dishKey = hourType + "\u0000" + productName;
DishStatisticsAccumulator dish = dishes.computeIfAbsent(dishKey,
key -> new DishStatisticsAccumulator(hourType, productName, car.getProductPic()));
+ if (car.getProductId() != null) {
+ dish.productIds.add(car.getProductId());
+ }
// 同餐段同名菜品合并时优先保留首个非空快照图片,兼容早期订单未保存图片的情况。
if (StrUtil.isBlank(dish.productPic) && StrUtil.isNotBlank(car.getProductPic())) {
dish.productPic = car.getProductPic();
@@ -1243,6 +1247,9 @@ public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalServ
/** 下单时固化的菜品名称。 */
private final String productName;
+ /** 同餐段、同名菜品实际涉及的商品 ID,保留首次出现顺序并去重,便于排查同名商品重复建档。 */
+ private final Set productIds = new LinkedHashSet<>();
+
/** 下单时固化的菜品图片地址;同名菜品聚合时保留首个非空值。 */
private String productPic;
@@ -1276,6 +1283,7 @@ public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalServ
HardCanteenUserApptDishStatisticsVo vo = new HardCanteenUserApptDishStatisticsVo();
vo.setHourType(hourType);
vo.setProductName(productName);
+ vo.setProductIds(new ArrayList<>(productIds));
vo.setProductPic(productPic);
vo.setProductCount(productCount);
vo.setOrderCount((long) orderIds.size());
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 735eaa76c..d41437794 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
@@ -9,6 +9,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;
@@ -47,6 +48,8 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.TemporalAdjusters;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@@ -194,7 +197,10 @@ public class HardOrderTotalServiceImpl implements HardOrderTotalService {
}
/**
- * 商户交易记录
+ * 汇总商户交易记录,并补全商户及商户部门展示信息。
+ *
+ * @param dto 商户、部门、用餐时段及交易时间等统计条件
+ * @return 按商户汇总的流水、退款和净消费数据;历史部门不存在时部门名为空
*/
@Override
public List listCs(HardOrderDto dto) {
@@ -202,85 +208,88 @@ public class HardOrderTotalServiceImpl implements HardOrderTotalService {
if (ObjectUtil.isEmpty(dto.getMchTypeCodes())) {
CurrentUserHardTypeInfoDto hardTypeInfo = hardManagerService.getCurrentUserHardTypeInfo(user);
dto.setMchTypeCodes(hardTypeInfo.getHardTypeList());
- } else {
- dto.setMchTypeCodes(dto.getMchTypeCodes());
}
- // 查询总流水
+
+ // 流水与退款的业务发生时间不同,必须分别查询后再按商户合并,避免直接关联造成一笔订单被多笔售后重复放大。
HardOrderDto totalDto = BeanUtil.toBean(dto, HardOrderDto.class);
totalDto.setOrderStatusCodes(HardOrderStatusEnum.getPayPrice());
DataScope totalDataScope = DataScope.of();
totalDataScope.setScopeDeptName("merchant_dept_id");
List totalList = hardOrderMapper.listCs(totalDto, totalDataScope);
- // 扣款总流水
+
HardOrderDto refundDto = BeanUtil.toBean(dto, HardOrderDto.class);
refundDto.setPayTimeList(null);
refundDto.setRefundTimeList(dto.getPayTimeList());
- refundDto.setOrderStatusCodes(HardOrderStatusEnum.getPayErrorCodes());
- refundDto.setRefundStatusCodes(List.of(HardSaleStatusEnum.COMPLETE.getStatus()));
+ // refundStatusCodes 属于订单退款状态,售后完成状态必须使用 saleStatusCodes,二者混用会把有效退款全部过滤掉。
+ refundDto.setRefundStatusCodes(null);
+ refundDto.setSaleStatusCodes(List.of(HardSaleStatusEnum.COMPLETE.getStatus()));
+ // 部分退款后订单仍是已支付状态,退款统计不能只查询整单退款状态的订单。
+ refundDto.setOrderStatusCodes(HardOrderStatusEnum.getPayPrice());
DataScope refundDataScope = DataScope.of();
refundDataScope.setScopeDeptName("merchant_dept_id");
-
List refundList = hardOrderMapper.listRefundCs(refundDto, refundDataScope);
- // 有退款没有流水的情况
- if (ObjectUtil.isAllNotEmpty(totalList, refundList)) {
- List totalIds = totalList.stream().map(HardOrderCsVo::getMerchantId).toList();
- for (HardOrderCsVo refund : refundList) {
- if (!totalIds.contains(refund.getMerchantId())) {
- HardOrderCsVo entity = BeanUtils.toBean(refund, HardOrderCsVo.class);
- entity.setTotalPayPrice(BigDecimal.ZERO);
- entity.setTotalPayCount(BigDecimal.ZERO);
- totalList.add(entity);
- }
- }
- }
- for (HardOrderCsVo v : totalList) {
- // 退款
- if (ObjectUtil.isNotEmpty(refundList)) {
- refundList.stream().filter(i -> ObjectUtil.equals(i.getMerchantId(), v.getMerchantId())).findFirst().ifPresent(i -> {
- v.setTotalRefundPrice(i.getTotalPayPrice());
- v.setTotalRefundCount(i.getTotalPayCount());
- });
- }
- // 消费净金额
- if (ObjectUtil.isNotEmpty(v.getTotalPayPrice())) {
- BigDecimal refundPrice = BigDecimal.ZERO;
- if (ObjectUtil.isNotEmpty(v.getTotalRefundPrice())) {
- refundPrice = v.getTotalRefundPrice();
- }
- v.setTotalProfitPrice(v.getTotalPayPrice().subtract(refundPrice));
- }
- // 消费净次数
- if (ObjectUtil.isNotEmpty(v.getTotalPayCount())) {
- BigDecimal refundCount = BigDecimal.ZERO;
- if (ObjectUtil.isNotEmpty(v.getTotalRefundCount())) {
- refundCount = v.getTotalRefundCount();
- }
- v.setTotalProfitCount(v.getTotalPayCount().subtract(refundCount));
- }
- }
- // 聚合一下数据
+
+ // 使用有序 Map 一次完成合并,同时保留原流水排序,并覆盖筛选期内只有退款、没有新支付的商户。
+ Map statisticsMap = new LinkedHashMap<>();
if (ObjectUtil.isNotEmpty(totalList)) {
+ for (HardOrderCsVo total : totalList) {
+ statisticsMap.put(total.getMerchantId(), total);
+ }
+ }
+ if (ObjectUtil.isNotEmpty(refundList)) {
+ for (HardOrderCsVo refund : refundList) {
+ HardOrderCsVo statistics = statisticsMap.computeIfAbsent(refund.getMerchantId(), merchantId -> {
+ HardOrderCsVo refundOnly = BeanUtils.toBean(refund, HardOrderCsVo.class);
+ refundOnly.setTotalPayPrice(BigDecimal.ZERO);
+ refundOnly.setTotalPayCount(BigDecimal.ZERO);
+ return refundOnly;
+ });
+ statistics.setTotalRefundPrice(ObjectUtil.defaultIfNull(refund.getTotalPayPrice(), BigDecimal.ZERO));
+ statistics.setTotalRefundCount(ObjectUtil.defaultIfNull(refund.getTotalPayCount(), BigDecimal.ZERO));
+ }
+ }
+
+ List statisticsList = new ArrayList<>(statisticsMap.values());
+ for (HardOrderCsVo statistics : statisticsList) {
+ BigDecimal totalPayPrice = ObjectUtil.defaultIfNull(statistics.getTotalPayPrice(), BigDecimal.ZERO);
+ BigDecimal totalPayCount = ObjectUtil.defaultIfNull(statistics.getTotalPayCount(), BigDecimal.ZERO);
+ BigDecimal totalRefundPrice = ObjectUtil.defaultIfNull(statistics.getTotalRefundPrice(), BigDecimal.ZERO);
+ BigDecimal totalRefundCount = ObjectUtil.defaultIfNull(statistics.getTotalRefundCount(), BigDecimal.ZERO);
+ statistics.setTotalPayPrice(totalPayPrice);
+ statistics.setTotalPayCount(totalPayCount);
+ statistics.setTotalRefundPrice(totalRefundPrice);
+ statistics.setTotalRefundCount(totalRefundCount);
+ statistics.setTotalProfitPrice(totalPayPrice.subtract(totalRefundPrice));
+ statistics.setTotalProfitCount(totalPayCount.subtract(totalRefundCount));
+ }
+
+ if (ObjectUtil.isNotEmpty(statisticsList)) {
// 商户
- List mchIds = totalList.stream().map(HardOrderCsVo::getMerchantId).distinct().toList();
+ List mchIds = statisticsList.stream().map(HardOrderCsVo::getMerchantId).filter(ObjectUtil::isNotEmpty).distinct().toList();
if (ObjectUtil.isNotEmpty(mchIds)) {
- List mchList = remoteMchService.getByIds(mchIds).getData();
- if (ObjectUtil.isNotEmpty(mchList)) {
- WalletDataUtil.convertMchList(totalList, CollectionUtils.convertMap(mchList, MchManageVo::getId));
+ R> mchResponse = remoteMchService.getByIds(mchIds);
+ if (ObjectUtil.isNotEmpty(mchResponse) && mchResponse.isOk() && ObjectUtil.isNotEmpty(mchResponse.getData())) {
+ WalletDataUtil.convertMchList(statisticsList, CollectionUtils.convertMap(mchResponse.getData(), MchManageVo::getId));
}
}
// 商户部门
- List mchDeptIds = totalList.stream().map(HardOrderCsVo::getMerchantDeptId).distinct().toList();
+ List mchDeptIds = statisticsList.stream().map(HardOrderCsVo::getMerchantDeptId).filter(ObjectUtil::isNotEmpty).distinct().toList();
if (ObjectUtil.isNotEmpty(mchDeptIds)) {
- Map deptMap = remoteDeptService.getDeptByDeptIds(mchDeptIds).getData();
- if (ObjectUtil.isNotEmpty(deptMap)) {
- for (HardOrderCsVo v : totalList) {
+ R
+ *
+ * @param userId 系统用户主键
+ * @param mchTypeList 允许匹配的商户类型集合
+ * @return 目标商户覆盖的用户部门主键集合;用户、所属部门、匹配商户或关联部门不存在时返回空集合
+ */
+ @Override
+ public List getSelfMchUserDeptIdListByUserId(Long userId, List mchTypeList) {
+ if (userId == null || ObjectUtil.isEmpty(mchTypeList)) {
+ return List.of();
+ }
+ R userResponse = remoteUserService.getInnerByUserId(userId);
+ if (userResponse == null || !userResponse.isOk() || userResponse.getData() == null
+ || userResponse.getData().getSysUser() == null || userResponse.getData().getSysUser().getDeptId() == null) {
+ return List.of();
+ }
+ MchManageVo merchant = getViewsCanteenMch(userResponse.getData().getSysUser().getDeptId(), mchTypeList);
+ if (merchant == null || merchant.getId() == null) {
+ return List.of();
+ }
+ return getSelfCanteenUserDeptIdList(merchant.getId());
+ }
+
/**
* 判断用户是否能查看到所有食堂
*/