Procházet zdrojové kódy

feat(order): 新增部门采购统计和采购明细功能

- 在OrderMainService中新增deptPurchase方法实现部门采购金额统计
- 在OrderMainService中新增purchaseDetail方法实现采购明细统计
- 添加DeptPurchaseVo和PurchaseDetailVo数据传输对象
- 在PcOrderController中新增deptPurchase和purchaseDetail接口
- 修改订单完成逻辑增加收货时间记录
- 在CustomerContactService中新增批量查询联系人信息方法
- 在RemoteCustomerContactService中新增远程调用批量查询方法
- 将商品销量字段类型从Long改为String以支持更多场景
- 实现按联系人/部门分组统计订单金额功能
- 实现购买数量、商品数量、品牌数量、品类数量统计
- 实现售后商品数量、平均完成时效、售后占比统计
hurx před 12 hodinami
rodič
revize
b302b69a53

+ 8 - 0
ruoyi-api/ruoyi-api-customer/src/main/java/org/dromara/customer/api/RemoteCustomerContactService.java

@@ -2,7 +2,15 @@ package org.dromara.customer.api;
 
 import org.dromara.customer.api.domain.vo.RemoteCustomerContactVo;
 
+import java.util.Map;
+import java.util.Set;
+
 public interface RemoteCustomerContactService {
 
     RemoteCustomerContactVo selectCustomerContactByCustomerIdAndUserId(Long customerId, Long userId);
+
+    /**
+     * 根据联系人ID批量查询联系人信息(含部门名称、联系人姓名)
+     */
+    Map<Long, RemoteCustomerContactVo> selectCustomerContactByIds(Set<Long> contactIds);
 }

+ 1 - 0
ruoyi-modules/ruoyi-bill/src/main/java/org/dromara/bill/controller/pc/PcStatementProductController.java

@@ -1,5 +1,6 @@
 package org.dromara.bill.controller.pc;
 
+import cn.hutool.core.util.ObjectUtil;
 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 import jakarta.validation.constraints.NotNull;
 import lombok.RequiredArgsConstructor;

+ 18 - 0
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/dubbo/RemoteCustomerContactServiceImpl.java

@@ -10,6 +10,11 @@ import org.dromara.customer.domain.vo.CustomerContactVo;
 import org.dromara.customer.service.ICustomerContactService;
 import org.springframework.stereotype.Service;
 
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
 @Slf4j
 @Service
 @RequiredArgsConstructor
@@ -23,4 +28,17 @@ public class RemoteCustomerContactServiceImpl implements RemoteCustomerContactSe
         CustomerContactVo customerContactVo = customerContactService.selectCustomerContactByCustomerIdAndUserId(customerId, userId);
         return BeanUtil.toBean(customerContactVo, RemoteCustomerContactVo.class);
     }
+
+    @Override
+    public Map<Long, RemoteCustomerContactVo> selectCustomerContactByIds(Set<Long> contactIds) {
+        if (contactIds == null || contactIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        return customerContactService.selectContactByIds(contactIds)
+            .entrySet().stream()
+            .collect(Collectors.toMap(
+                Map.Entry::getKey,
+                e -> BeanUtil.toBean(e.getValue(), RemoteCustomerContactVo.class)
+            ));
+    }
 }

+ 10 - 0
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/ICustomerContactService.java

@@ -10,6 +10,8 @@ import org.dromara.common.mybatis.core.page.PageQuery;
 
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 /**
  * 客户联系人信息Service接口
@@ -35,6 +37,14 @@ public interface ICustomerContactService extends IService<CustomerContact> {
      */
     CustomerContactVo selectCustomerContactByCustomerIdAndUserId(Long customerId, Long userId);
 
+    /**
+     * 根据联系人ID批量查询联系人信息
+     *
+     * @param contactIds 联系人ID集合
+     * @return key=contactId, value=CustomerContactVo
+     */
+    Map<Long, CustomerContactVo> selectContactByIds(Set<Long> contactIds);
+
     /**
      * 分页查询客户联系人信息列表
      *

+ 11 - 0
ruoyi-modules/ruoyi-customer/src/main/java/org/dromara/customer/service/impl/CustomerContactServiceImpl.java

@@ -89,6 +89,17 @@ public class CustomerContactServiceImpl extends ServiceImpl<CustomerContactMappe
         return baseMapper.selectVoOne(new LambdaQueryWrapper<CustomerContact>().eq(CustomerContact::getCustomerId, customerId).eq(CustomerContact::getUserId, userId).eq(CustomerContact::getDelFlag, '0').last("limit 1"));
     }
 
+    @Override
+    public Map<Long, CustomerContactVo> selectContactByIds(Set<Long> contactIds) {
+        if (contactIds == null || contactIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        List<CustomerContactVo> list = baseMapper.selectVoByIds(contactIds);
+        return list.stream()
+            .filter(vo -> vo != null && vo.getId() != null)
+            .collect(Collectors.toMap(CustomerContactVo::getId, vo -> vo, (k1, k2) -> k1));
+    }
+
     /**
      * 分页查询客户联系人信息列表
      *

+ 1 - 1
ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/domain/EpAdModuleItem.java

@@ -68,7 +68,7 @@ public class EpAdModuleItem extends TenantEntity {
     /**
      * 销量
      */
-    private Long salesCount;
+    private String salesCount;
 
     /**
      * 排序

+ 1 - 1
ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/domain/bo/EpAdModuleItemBo.java

@@ -67,7 +67,7 @@ public class EpAdModuleItemBo extends BaseEntity {
     /**
      * 销量
      */
-    private Long salesCount;
+    private String salesCount;
 
     /**
      * 排序

+ 1 - 1
ruoyi-modules/ruoyi-mall/src/main/java/org/dromara/mall/domain/vo/EpAdModuleItemVo.java

@@ -83,7 +83,7 @@ public class EpAdModuleItemVo implements Serializable {
      * 销量
      */
     @ExcelProperty(value = "销量")
-    private Long salesCount;
+    private String salesCount;
 
     /**
      * 排序

+ 32 - 1
ruoyi-modules/ruoyi-order/src/main/java/org/dromara/order/controller/pc/PcOrderController.java

@@ -25,11 +25,13 @@ import org.dromara.order.domain.OrderCustomerFlowLink;
 import org.dromara.order.domain.OrderCustomerFlowNodeLink;
 import org.dromara.order.domain.bo.*;
 import org.dromara.order.domain.dto.OrderPayDto;
-import org.dromara.order.domain.vo.OrderCountVo;
 import org.dromara.order.domain.vo.CustomerOrderTradeDataVo;
+import org.dromara.order.domain.vo.DeptPurchaseVo;
+import org.dromara.order.domain.vo.OrderCountVo;
 import org.dromara.order.domain.vo.OrderMainVo;
 import org.dromara.order.domain.vo.OrderProductVo;
 import org.dromara.order.domain.vo.OrderStatusStats;
+import org.dromara.order.domain.vo.PurchaseDetailVo;
 import org.dromara.order.service.IOrderCustomerFlowLinkService;
 import org.dromara.order.service.IOrderCustomerFlowNodeLinkService;
 import org.dromara.order.service.IOrderCustomerFlowService;
@@ -526,4 +528,33 @@ public class PcOrderController extends BaseController {
 
         return R.ok();
     }
+
+    /**
+     * 部门采购金额统计
+     * 查询当前客户所有订单,按联系人/部门分组统计下单金额、已完成金额、待完成金额
+     *
+     * @param contactId 下单人ID(可选,筛选指定联系人)
+     */
+    @GetMapping("/deptPurchase")
+    public TableDataInfo<DeptPurchaseVo> deptPurchase(@RequestParam(required = false) Long contactId, PageQuery pageQuery) {
+        Long customerId = LoginHelper.getLoginUser().getCustomerId();
+        if (ObjectUtil.isEmpty(customerId)) {
+            return  TableDataInfo.build();
+        }
+        return orderMainService.deptPurchase(customerId, contactId, pageQuery);
+    }
+
+    /**
+     * 采购明细统计
+     * 统计当前客户订单的购买数量、商品数量、品牌数量、品类数量、售后商品数量、平均完成时效、售后商品占比
+     */
+    @GetMapping("/purchaseDetail")
+    public R<PurchaseDetailVo> purchaseDetail() {
+        Long customerId = LoginHelper.getLoginUser().getCustomerId();
+        if (ObjectUtil.isEmpty(customerId)) {
+            return R.fail("未获取到当前客户信息");
+        }
+        return orderMainService.purchaseDetail(customerId);
+    }
+
 }

+ 45 - 0
ruoyi-modules/ruoyi-order/src/main/java/org/dromara/order/domain/vo/DeptPurchaseVo.java

@@ -0,0 +1,45 @@
+package org.dromara.order.domain.vo;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 部门采购金额 VO
+ *
+ * @author Claude
+ * @date 2026-06-08
+ */
+@Data
+public class DeptPurchaseVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 部门名称
+     */
+    private String deptName;
+
+    /**
+     * 下单人(联系人姓名)
+     */
+    private String orderPerson;
+
+    /**
+     * 下单金额(所有订单总金额)
+     */
+    private BigDecimal orderAmount;
+
+    /**
+     * 已完成订单金额(orderStatus >= 5)
+     */
+    private BigDecimal completedAmount;
+
+    /**
+     * 待完成订单金额(orderStatus < 5)
+     */
+    private BigDecimal pendingAmount;
+}

+ 52 - 0
ruoyi-modules/ruoyi-order/src/main/java/org/dromara/order/domain/vo/PurchaseDetailVo.java

@@ -0,0 +1,52 @@
+package org.dromara.order.domain.vo;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 采购明细统计VO
+ *
+ * @author LionLi
+ * @date 2026-06-08
+ */
+@Data
+public class PurchaseDetailVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 统计数据:购买数量、商品数量、品牌数量、品类数量
+     */
+    private List<StatItem> statData;
+
+    /**
+     * 售后数据:售后商品数量、平均完成时效、售后商品占比
+     */
+    private List<AfterSaleItem> afterSaleData;
+
+    @Data
+    @Accessors(chain = true)
+    public static class StatItem implements Serializable {
+        @Serial
+        private static final long serialVersionUID = 1L;
+        private String value;
+        private String label;
+    }
+
+    @Data
+    @Accessors(chain = true)
+    public static class AfterSaleItem implements Serializable {
+        @Serial
+        private static final long serialVersionUID = 1L;
+        private String label;
+        private String value;
+        private List<Integer> data;
+        private String color;
+        private String type;
+    }
+}

+ 18 - 0
ruoyi-modules/ruoyi-order/src/main/java/org/dromara/order/service/IOrderMainService.java

@@ -179,4 +179,22 @@ public interface IOrderMainService extends IService<OrderMain> {
 
     R<CustomerOrderTradeDataVo> customerOrderTradeData(Long customerId);
 
+    /**
+     * 部门采购金额统计(按联系人/部门分组,支持分页)
+     *
+     * @param customerId 客户ID
+     * @param contactId  联系人ID(可选,筛选指定下单人)
+     * @param pageQuery  分页参数
+     * @return 部门采购统计分页列表
+     */
+    TableDataInfo<DeptPurchaseVo> deptPurchase(Long customerId, Long contactId, PageQuery pageQuery);
+
+    /**
+     * 采购明细统计(购买数量、商品数量、品牌数量、品类数量、售后统计)
+     *
+     * @param customerId 客户ID
+     * @return 采购明细统计VO
+     */
+    R<PurchaseDetailVo> purchaseDetail(Long customerId);
+
 }

+ 211 - 1
ruoyi-modules/ruoyi-order/src/main/java/org/dromara/order/service/impl/OrderMainServiceImpl.java

@@ -1204,7 +1204,7 @@ public class OrderMainServiceImpl extends ServiceImpl<OrderMainMapper, OrderMain
         LambdaUpdateWrapper<OrderMain> updateWrapper = new LambdaUpdateWrapper<>();
 
         // 设置更新后的值
-        updateWrapper.set(OrderMain::getOrderStatus, OrderStatus.COMPLETED.getCode());
+        updateWrapper.set(OrderMain::getOrderStatus, OrderStatus.COMPLETED.getCode()).set(OrderMain::getReceivingTime, new Date());
 
         // 构建 WHERE 条件逻辑:
         // (id IN (...) OR parentOrderId IN (...)) AND orderStatus = SHIPPED
@@ -2288,4 +2288,214 @@ public class OrderMainServiceImpl extends ServiceImpl<OrderMainMapper, OrderMain
 
         return R.ok(vo);
     }
+
+    @Override
+    public TableDataInfo<DeptPurchaseVo> deptPurchase(Long customerId, Long contactId, PageQuery pageQuery) {
+        // 1. 查询当前客户的所有非子单订单
+        LambdaQueryWrapper<OrderMain> wrapper = new LambdaQueryWrapper<OrderMain>()
+            .eq(OrderMain::getCustomerId, customerId)
+            .eq(OrderMain::getCurrentLevel, 1)
+            .eq(OrderMain::getDelFlag, "0");
+        // 可选:按下单人筛选
+        if (contactId != null) {
+            wrapper.eq(OrderMain::getContactId, contactId);
+        }
+        List<OrderMain> orders = baseMapper.selectList(wrapper
+            .select(OrderMain::getTotalAmount, OrderMain::getOrderStatus, OrderMain::getContactId));
+
+        if (CollUtil.isEmpty(orders)) {
+            return new TableDataInfo<>(Collections.emptyList(), 0);
+        }
+
+        // 2. 按 contactId 分组,计算金额
+        Map<Long, BigDecimal[]> amountMap = new LinkedHashMap<>(); // [totalAmount, completedAmount, pendingAmount]
+        for (OrderMain order : orders) {
+            Long orderContactId = order.getContactId();
+            if (orderContactId == null) {
+                continue;
+            }
+            BigDecimal orderAmount = order.getTotalAmount() != null ? order.getTotalAmount() : BigDecimal.ZERO;
+            BigDecimal[] amounts = amountMap.computeIfAbsent(orderContactId, k -> new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO});
+            amounts[0] = amounts[0].add(orderAmount); // 总金额
+            // orderStatus >= 5 为已完成
+            if (Integer.parseInt(order.getOrderStatus()) >= 5) {
+                amounts[1] = amounts[1].add(orderAmount); // 已完成
+            } else {
+                amounts[2] = amounts[2].add(orderAmount); // 待完成
+            }
+        }
+
+        if (amountMap.isEmpty()) {
+            return new TableDataInfo<>(Collections.emptyList(), 0);
+        }
+
+        // 3. 远程批量查询联系人信息(含部门名称、联系人姓名)
+        Map<Long, RemoteCustomerContactVo> contactMap = remoteCustomerContactService.selectCustomerContactByIds(amountMap.keySet());
+
+        // 4. 组装结果列表
+        List<DeptPurchaseVo> allList = new ArrayList<>();
+        for (Map.Entry<Long, BigDecimal[]> entry : amountMap.entrySet()) {
+            Long ctId = entry.getKey();
+            BigDecimal[] amounts = entry.getValue();
+
+            DeptPurchaseVo vo = new DeptPurchaseVo();
+            RemoteCustomerContactVo contact = contactMap.get(ctId);
+            if (contact != null) {
+                vo.setDeptName(contact.getDeptName());
+                vo.setOrderPerson(contact.getContactName());
+            }
+            vo.setOrderAmount(amounts[0]);
+            vo.setCompletedAmount(amounts[1]);
+            vo.setPendingAmount(amounts[2]);
+            allList.add(vo);
+        }
+
+        // 5. 内存分页
+        int total = allList.size();
+        int pageNum = pageQuery != null ? pageQuery.getPageNum() : 1;
+        int pageSize = pageQuery != null ? pageQuery.getPageSize() : 10;
+        int fromIndex = (pageNum - 1) * pageSize;
+        if (fromIndex >= total) {
+            return new TableDataInfo<>(Collections.emptyList(), total);
+        }
+        int toIndex = Math.min(fromIndex + pageSize, total);
+        List<DeptPurchaseVo> pageList = allList.subList(fromIndex, toIndex);
+
+        return new TableDataInfo<>(pageList, total);
+    }
+
+    @Override
+    public R<PurchaseDetailVo> purchaseDetail(Long customerId) {
+        PurchaseDetailVo vo = new PurchaseDetailVo();
+
+        // ========== 1. 查询当前客户的非子单订单 ==========
+        List<OrderMain> orders = baseMapper.selectList(new LambdaQueryWrapper<OrderMain>()
+            .eq(OrderMain::getCustomerId, customerId)
+            .eq(OrderMain::getCurrentLevel, 1)
+            .eq(OrderMain::getDelFlag, "0"));
+
+        long orderCount = orders.size(); // 购买数量(件)
+
+        if (CollUtil.isEmpty(orders)) {
+            vo.setStatData(buildStatData("0", "0", "0", "0"));
+            vo.setAfterSaleData(buildAfterSaleData("0", "0", "0%",
+                Collections.emptyList(), Collections.emptyList(), Collections.emptyList()));
+            return R.ok(vo);
+        }
+
+        List<Long> orderIds = orders.stream().map(OrderMain::getId).collect(Collectors.toList());
+
+        // ========== 2. 查询订单商品(去重产品数、品牌数、品类数) ==========
+        List<OrderProduct> orderProducts = orderProductMapper.selectList(new LambdaQueryWrapper<OrderProduct>()
+            .in(OrderProduct::getOrderId, orderIds)
+            .eq(OrderProduct::getDelFlag, "0"));
+
+        Set<Long> productIds = orderProducts.stream()
+            .map(OrderProduct::getProductId)
+            .filter(Objects::nonNull)
+            .collect(Collectors.toSet());
+        long productCount = productIds.size(); // 商品数量(件)
+
+        // 远程查询商品品牌和品类
+        long brandCount = 0;
+        long categoryCount = 0;
+        if (CollUtil.isNotEmpty(productIds)) {
+            List<ProductVo> productVos = remoteProductService.getProductDetails(new ArrayList<>(productIds));
+            if (CollUtil.isNotEmpty(productVos)) {
+                brandCount = productVos.stream()
+                    .map(ProductVo::getBrandId)
+                    .filter(Objects::nonNull)
+                    .distinct()
+                    .count();
+                // 三类品类 = 去重的底级分类
+                categoryCount = productVos.stream()
+                    .map(ProductVo::getBottomCategoryId)
+                    .filter(Objects::nonNull)
+                    .distinct()
+                    .count();
+            }
+        }
+
+        // ========== 3. 售后统计 ==========
+        // 售后商品数量 = sum(returnProductNum)
+        List<OrderReturn> orderReturns = orderReturnMapper.selectList(new LambdaQueryWrapper<OrderReturn>()
+            .eq(OrderReturn::getCustomerId, customerId)
+            .eq(OrderReturn::getDelFlag, "0"));
+        long afterSaleProductCount = orderReturns.stream()
+            .map(OrderReturn::getReturnProductNum)
+            .filter(Objects::nonNull)
+            .mapToLong(Long::longValue)
+            .sum();
+
+        // 售后商品占比 = 售后商品数 / 商品总数 * 100%
+        String afterSaleRatio;
+        if (productCount > 0) {
+            BigDecimal ratio = BigDecimal.valueOf(afterSaleProductCount)
+                .multiply(BigDecimal.valueOf(100))
+                .divide(BigDecimal.valueOf(productCount), 1, java.math.RoundingMode.HALF_UP);
+            afterSaleRatio = ratio.stripTrailingZeros().toPlainString() + "%";
+        } else {
+            afterSaleRatio = "0%";
+        }
+
+        // 平均完成时效 = average(receivingTime - createTime)  for 已完成订单(orderStatus >= 5)
+        long avgDays = 0;
+        List<OrderMain> completedOrders = orders.stream()
+            .filter(o -> o.getReceivingTime() != null && o.getCreateTime() != null
+                && Integer.parseInt(o.getOrderStatus()) >= 5)
+            .collect(Collectors.toList());
+        if (!completedOrders.isEmpty()) {
+            long totalDays = completedOrders.stream()
+                .mapToLong(o -> {
+                    long diffMillis = o.getReceivingTime().getTime() - o.getCreateTime().getTime();
+                    return Math.max(0, diffMillis / (1000 * 60 * 60 * 24));
+                })
+                .sum();
+            avgDays = totalDays / completedOrders.size();
+        }
+
+        // ========== 4. 组装返回数据 ==========
+        vo.setStatData(buildStatData(
+            String.valueOf(orderCount),
+            String.valueOf(productCount),
+            String.valueOf(brandCount),
+            String.valueOf(categoryCount)
+        ));
+        vo.setAfterSaleData(buildAfterSaleData(
+            String.valueOf(afterSaleProductCount),
+            String.valueOf(avgDays),
+            afterSaleRatio,
+            Collections.emptyList(),
+            Collections.emptyList(),
+            Collections.emptyList()
+        ));
+
+        return R.ok(vo);
+    }
+
+    private List<PurchaseDetailVo.StatItem> buildStatData(String orderCount, String productCount,
+                                                           String brandCount, String categoryCount) {
+        List<PurchaseDetailVo.StatItem> list = new ArrayList<>();
+        list.add(new PurchaseDetailVo.StatItem().setValue(orderCount).setLabel("购买数量(件)"));
+        list.add(new PurchaseDetailVo.StatItem().setValue(productCount).setLabel("商品数量(件)"));
+        list.add(new PurchaseDetailVo.StatItem().setValue(brandCount).setLabel("品牌数量(件)"));
+        list.add(new PurchaseDetailVo.StatItem().setValue(categoryCount).setLabel("三类品类数量(件)"));
+        return list;
+    }
+
+    private List<PurchaseDetailVo.AfterSaleItem> buildAfterSaleData(String afterSaleCount, String avgDays,
+                                                                     String ratio, List<Integer> data1,
+                                                                     List<Integer> data2, List<Integer> data3) {
+        List<PurchaseDetailVo.AfterSaleItem> list = new ArrayList<>();
+        list.add(new PurchaseDetailVo.AfterSaleItem()
+            .setLabel("售后商品数量(件)").setValue(afterSaleCount)
+            .setData(data1).setColor("#e60012").setType("line"));
+        list.add(new PurchaseDetailVo.AfterSaleItem()
+            .setLabel("平均完成时效(天)").setValue(avgDays)
+            .setData(data2).setColor("#3498db").setType("bar"));
+        list.add(new PurchaseDetailVo.AfterSaleItem()
+            .setLabel("售后商品占比").setValue(ratio)
+            .setData(data3).setColor("#f4c542").setType("line"));
+        return list;
+    }
 }