From df823119ed6fada0e43036d06952ca1f97c75f34 Mon Sep 17 00:00:00 2001 From: cst61 Date: Fri, 19 Jun 2026 21:13:56 +0800 Subject: [PATCH] =?UTF-8?q?=E9=83=A8=E9=97=A8=E4=BA=A4=E6=98=93=E9=87=91?= =?UTF-8?q?=E9=A2=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LbDepartmentUserController.java | 23 ++ .../java/com/rj/mapper/LbOrderRowMapper.java | 18 + .../rj/service/ILbDepartmentUserService.java | 9 + .../impl/LbDepartmentUserServiceImpl.java | 311 ++++++++++++++++++ .../resources/mapper/LbOrderRowMapper.xml | 33 ++ 5 files changed, 394 insertions(+) diff --git a/src/main/java/com/rj/controller/LbDepartmentUserController.java b/src/main/java/com/rj/controller/LbDepartmentUserController.java index 8d96ef0..9b218fc 100644 --- a/src/main/java/com/rj/controller/LbDepartmentUserController.java +++ b/src/main/java/com/rj/controller/LbDepartmentUserController.java @@ -144,6 +144,29 @@ public class LbDepartmentUserController { return ResponseEntity.badRequest().body(result); } + @GetMapping("/treeSumTotalMoney") + @Operation(summary = "部门用户树形结构(累计交易金额)") + public ResponseEntity> treeSumTotalMoney( + @Parameter(description = "租户id", required = true) @RequestParam String tenantId, + @Parameter(description = "是否活跃:1活跃 0不活跃") @RequestParam(name = "is_active", required = false) Integer isActive, + @Parameter(description = "加入日期,查询 join_date 大于该日期的数据") + @RequestParam(name = "join_date", required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate joinDate, + @Parameter(description = "最后成交日期,查询 last_trade_date 大于该日期的数据") + @RequestParam(name = "last_trade_date", required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate lastTradeDate, + @Parameter(description = "交易开始时间(buy_time),格式:yyyy-MM-dd HH:mm:ss") + @RequestParam(name = "buy_start_time", required = false) String buyStartTime, + @Parameter(description = "交易结束时间(buy_time),格式:yyyy-MM-dd HH:mm:ss") + @RequestParam(name = "buy_end_time", required = false) String buyEndTime) { + Map result = lbDepartmentUserService.getDepartmentUserTreeCopy(tenantId, isActive, joinDate, lastTradeDate, buyStartTime, buyEndTime); + Boolean success = (Boolean) result.get("success"); + if (success != null && success) { + return ResponseEntity.ok(result); + } + return ResponseEntity.badRequest().body(result); + } + @PostMapping("/exportUsers") @Operation(summary = "导出指定用户Excel", description = "按租户ID与用户ID列表导出:用户ID、用户名、手机号、加入时间、最早/最晚buy_time、累计交易金额") diff --git a/src/main/java/com/rj/mapper/LbOrderRowMapper.java b/src/main/java/com/rj/mapper/LbOrderRowMapper.java index dd7d779..d9b347a 100644 --- a/src/main/java/com/rj/mapper/LbOrderRowMapper.java +++ b/src/main/java/com/rj/mapper/LbOrderRowMapper.java @@ -7,7 +7,9 @@ import com.rj.entity.LbOrderRow; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +import java.math.BigDecimal; import java.util.List; +import java.util.Map; @Mapper public interface LbOrderRowMapper extends BaseMapper { @@ -23,4 +25,20 @@ public interface LbOrderRowMapper extends BaseMapper { */ List selectLatestPaidStatsByTenantId(@Param("tenantId") String tenantId, @Param("dataType") String dataType); + + /** + * 按租户、买家ID列表和时间范围查询订单总金额。 + */ + BigDecimal selectSumTotalMoneyByBuyerIdsAndTimeRange(@Param("tenantId") String tenantId, + @Param("buyerIds") List buyerIds, + @Param("buyStartTime") String buyStartTime, + @Param("buyEndTime") String buyEndTime); + + /** + * 按租户、买家ID列表和时间范围分组查询每个买家的订单总金额。 + */ + List> selectSumTotalMoneyGroupByBuyerIds(@Param("tenantId") String tenantId, + @Param("buyerIds") List buyerIds, + @Param("buyStartTime") String buyStartTime, + @Param("buyEndTime") String buyEndTime); } diff --git a/src/main/java/com/rj/service/ILbDepartmentUserService.java b/src/main/java/com/rj/service/ILbDepartmentUserService.java index fd86e88..2fceab8 100644 --- a/src/main/java/com/rj/service/ILbDepartmentUserService.java +++ b/src/main/java/com/rj/service/ILbDepartmentUserService.java @@ -46,8 +46,17 @@ public interface ILbDepartmentUserService extends IService { */ Map getDepartmentUserTree(String tenantId, Integer isActive, LocalDate joinDate, LocalDate lastTradeDate); + /** + * 按租户从 lb_user 构建父子树(克隆) + */ + Map getDepartmentUserTreeCopy(String tenantId, Integer isActive, LocalDate joinDate, LocalDate lastTradeDate, + String buyStartTime, String buyEndTime); + /** * 按租户与用户 ID 列表导出部门用户及订单汇总 Excel。 */ void exportUsersExcel(String tenantId, List userIds, HttpServletResponse response); + + Map getDepartmentUserTreeSumTotalMoney(String tenantId, Integer isActive, LocalDate joinDate, + LocalDate lastTradeDate, String buyStartTime, String buyEndTime); } diff --git a/src/main/java/com/rj/service/impl/LbDepartmentUserServiceImpl.java b/src/main/java/com/rj/service/impl/LbDepartmentUserServiceImpl.java index 82916e9..300ca36 100644 --- a/src/main/java/com/rj/service/impl/LbDepartmentUserServiceImpl.java +++ b/src/main/java/com/rj/service/impl/LbDepartmentUserServiceImpl.java @@ -761,6 +761,170 @@ public class LbDepartmentUserServiceImpl extends ServiceImpl getDepartmentUserTreeCopy(String tenantId, Integer isActive, LocalDate joinDate, LocalDate lastTradeDate, + String buyStartTime, String buyEndTime) { + Map result = new HashMap<>(); + try { + if (tenantId == null || tenantId.trim().isEmpty()) { + result.put("success", false); + result.put("message", "租户id不能为空"); + return result; + } + String tenantIdTrim = tenantId.trim(); + + List lbUsers = lbUserMapper.selectList( + new LambdaQueryWrapper().eq(LbUser::getTenantId, tenantIdTrim)); + if (lbUsers == null || lbUsers.isEmpty()) { + result.put("success", true); + result.put("message", "查询成功"); + result.put("data", new ArrayList<>()); + result.put("total", 0); + result.put("tenantId", tenantIdTrim); + return result; + } + + Map userById = new LinkedHashMap<>(); + for (LbUser lbUser : lbUsers) { + if (lbUser.getId() != null) { + userById.put(String.valueOf(lbUser.getId()), lbUser); + } + } + + Map tradeStatsByUserId = loadBuyerTradeStatsFromOrderRow(tenantIdTrim); + boolean hasFilter = isActive != null || joinDate != null || lastTradeDate != null; + + Set matchedUserIds = new HashSet<>(); + Map joinDateByUserId = new HashMap<>(); + Map lastTradeDateByUserId = new HashMap<>(); + Map lastBuyAmtByUserId = new HashMap<>(); + Map isActiveByUserId = new HashMap<>(); + for (LbUser lbUser : lbUsers) { + if (lbUser.getId() == null) { + continue; + } + String userId = String.valueOf(lbUser.getId()); + LocalDate userJoinDate = lbUser.getJoinTime() != null + ? lbUser.getJoinTime().toLocalDate() : null; + LbBuyerTradeStats tradeStats = tradeStatsByUserId.get(userId); + LocalDate userLastTradeDate = tradeStats == null + ? null : parseOrderTimeToDate(tradeStats.getPayTime()); + BigDecimal userLastBuyAmt = tradeStats == null ? null : tradeStats.getTotalMoney(); + int userIsActive = tradeStats == null ? 0 : 1; + + joinDateByUserId.put(userId, userJoinDate); + lastTradeDateByUserId.put(userId, userLastTradeDate); + lastBuyAmtByUserId.put(userId, userLastBuyAmt); + isActiveByUserId.put(userId, userIsActive); + + if (!hasFilter || matchesTreeFilter( + userIsActive, userJoinDate, userLastTradeDate, isActive, joinDate, lastTradeDate)) { + matchedUserIds.add(userId); + } + } + + Set visibleUserIds = hasFilter + ? includeAncestorUserIds(matchedUserIds, userById) + : new HashSet<>(userById.keySet()); + + Map> nodeByUserId = new LinkedHashMap<>(); + Map parentByUserId = new HashMap<>(); + Map> childrenByUserId = new HashMap<>(); + + for (String userId : visibleUserIds) { + LbUser lbUser = userById.get(userId); + if (lbUser == null) { + continue; + } + nodeByUserId.put(userId, toLbUserTreeNode( + lbUser, + joinDateByUserId.get(userId), + lastTradeDateByUserId.get(userId), + lastBuyAmtByUserId.get(userId), + isActiveByUserId.getOrDefault(userId, 0))); + + String parentUserId = resolveLbUserParentId(lbUser, userById); + if (parentUserId != null && visibleUserIds.contains(parentUserId)) { + parentByUserId.put(userId, parentUserId); + childrenByUserId.computeIfAbsent(parentUserId, k -> new ArrayList<>()).add(userId); + } + } + + boolean hasTradeTimeFilter = (buyStartTime != null && !buyStartTime.trim().isEmpty()) + || (buyEndTime != null && !buyEndTime.trim().isEmpty()); + + Map tradeMoneySumByUserId = new HashMap<>(); + Map userTradeMoney = new HashMap<>(); + if (hasTradeTimeFilter) { + if (!visibleUserIds.isEmpty()) { + List> tradeResults = lbOrderRowMapper.selectSumTotalMoneyGroupByBuyerIds( + tenantIdTrim, new ArrayList<>(visibleUserIds), buyStartTime, buyEndTime); + + for (Map row : tradeResults) { + String buyerId = String.valueOf(row.get("buyerId")); + BigDecimal totalMoney = (BigDecimal) row.get("totalMoney"); + userTradeMoney.put(buyerId, totalMoney != null ? totalMoney : BigDecimal.ZERO); + } + + for (String userId : visibleUserIds) { + BigDecimal sumAmount = calculateRecursiveTradeSumOptimized(userId, childrenByUserId, userTradeMoney); + tradeMoneySumByUserId.put(userId, sumAmount); + } + } + } + + List> treeRoots = new ArrayList<>(); + for (Map.Entry> entry : nodeByUserId.entrySet()) { + String userId = entry.getKey(); + Map node = entry.getValue(); + + if (hasTradeTimeFilter) { + node.put("selfTradeMoney", userTradeMoney.getOrDefault(userId, BigDecimal.ZERO)); + node.put("tradeMoneySum", tradeMoneySumByUserId.getOrDefault(userId, BigDecimal.ZERO)); + } + + String parentUserId = parentByUserId.get(userId); + Map parentNode = parentUserId == null ? null : nodeByUserId.get(parentUserId); + if (parentNode == null) { + treeRoots.add(node); + continue; + } + @SuppressWarnings("unchecked") + List> children = (List>) parentNode.get("children"); + children.add(node); + } + sortDepartmentUserTreeByName(treeRoots); + List> treeData = skipFirstTreeLevel(treeRoots); + + result.put("success", true); + result.put("message", "查询成功"); + result.put("data", treeData); + result.put("total", countTreeNodes(treeData)); + result.put("tenantId", tenantIdTrim); + return result; + } catch (Exception e) { + result.put("success", false); + result.put("message", "查询树形结构异常:" + e.getMessage()); + return result; + } + } + + /** + * 优化后的递归计算:从内存 Map 中获取数据,避免重复数据库查询 + */ + private BigDecimal calculateRecursiveTradeSumOptimized(String userId, Map> childrenByUserId, + Map userTradeMoney) { + BigDecimal sum = userTradeMoney.getOrDefault(userId, BigDecimal.ZERO); + + List children = childrenByUserId.get(userId); + if (children != null && !children.isEmpty()) { + for (String childId : children) { + sum = sum.add(calculateRecursiveTradeSumOptimized(childId, childrenByUserId, userTradeMoney)); + } + } + return sum; + } + /** * 去掉第 1 级根节点,将其 children 提升为返回给前端的顶层。 */ @@ -1135,6 +1299,153 @@ public class LbDepartmentUserServiceImpl extends ServiceImpl getDepartmentUserTreeSumTotalMoney(String tenantId, Integer isActive, LocalDate joinDate, LocalDate lastTradeDate, String buyStartTime, String buyEndTime) { + Map result = new HashMap<>(); + try { + if (tenantId == null || tenantId.trim().isEmpty()) { + result.put("success", false); + result.put("message", "租户id不能为空"); + return result; + } + String tenantIdTrim = tenantId.trim(); + + List lbUsers = lbUserMapper.selectList( + new LambdaQueryWrapper().eq(LbUser::getTenantId, tenantIdTrim)); + if (lbUsers == null || lbUsers.isEmpty()) { + result.put("success", true); + result.put("message", "查询成功"); + result.put("data", new ArrayList<>()); + result.put("total", 0); + result.put("tenantId", tenantIdTrim); + return result; + } + + Map userById = new LinkedHashMap<>(); + for (LbUser lbUser : lbUsers) { + if (lbUser.getId() != null) { + userById.put(String.valueOf(lbUser.getId()), lbUser); + } + } + + Map tradeStatsByUserId = loadBuyerTradeStatsFromOrderRow(tenantIdTrim); + boolean hasFilter = isActive != null || joinDate != null || lastTradeDate != null; + + Set matchedUserIds = new HashSet<>(); + Map joinDateByUserId = new HashMap<>(); + Map lastTradeDateByUserId = new HashMap<>(); + Map lastBuyAmtByUserId = new HashMap<>(); + Map isActiveByUserId = new HashMap<>(); + for (LbUser lbUser : lbUsers) { + if (lbUser.getId() == null) { + continue; + } + String userId = String.valueOf(lbUser.getId()); + LocalDate userJoinDate = lbUser.getJoinTime() != null + ? lbUser.getJoinTime().toLocalDate() : null; + LbBuyerTradeStats tradeStats = tradeStatsByUserId.get(userId); + LocalDate userLastTradeDate = tradeStats == null + ? null : parseOrderTimeToDate(tradeStats.getPayTime()); + BigDecimal userLastBuyAmt = tradeStats == null ? null : tradeStats.getTotalMoney(); + int userIsActive = tradeStats == null ? 0 : 1; + + joinDateByUserId.put(userId, userJoinDate); + lastTradeDateByUserId.put(userId, userLastTradeDate); + lastBuyAmtByUserId.put(userId, userLastBuyAmt); + isActiveByUserId.put(userId, userIsActive); + + if (!hasFilter || matchesTreeFilter( + userIsActive, userJoinDate, userLastTradeDate, isActive, joinDate, lastTradeDate)) { + matchedUserIds.add(userId); + } + } + + Set visibleUserIds = hasFilter + ? includeAncestorUserIds(matchedUserIds, userById) + : new HashSet<>(userById.keySet()); + + Map> nodeByUserId = new LinkedHashMap<>(); + Map parentByUserId = new HashMap<>(); + Map> childrenByUserId = new HashMap<>(); + + for (String userId : visibleUserIds) { + LbUser lbUser = userById.get(userId); + if (lbUser == null) { + continue; + } + nodeByUserId.put(userId, toLbUserTreeNode( + lbUser, + joinDateByUserId.get(userId), + lastTradeDateByUserId.get(userId), + lastBuyAmtByUserId.get(userId), + isActiveByUserId.getOrDefault(userId, 0))); + + String parentUserId = resolveLbUserParentId(lbUser, userById); + if (parentUserId != null && visibleUserIds.contains(parentUserId)) { + parentByUserId.put(userId, parentUserId); + childrenByUserId.computeIfAbsent(parentUserId, k -> new ArrayList<>()).add(userId); + } + } + + boolean hasTradeTimeFilter = (buyStartTime != null && !buyStartTime.trim().isEmpty()) + || (buyEndTime != null && !buyEndTime.trim().isEmpty()); + + Map tradeMoneySumByUserId = new HashMap<>(); + Map userTradeMoney = new HashMap<>(); + if (hasTradeTimeFilter) { + if (!visibleUserIds.isEmpty()) { + List> tradeResults = lbOrderRowMapper.selectSumTotalMoneyGroupByBuyerIds( + tenantIdTrim, new ArrayList<>(visibleUserIds), buyStartTime, buyEndTime); + + for (Map row : tradeResults) { + String buyerId = String.valueOf(row.get("buyerId")); + BigDecimal totalMoney = (BigDecimal) row.get("totalMoney"); + userTradeMoney.put(buyerId, totalMoney != null ? totalMoney : BigDecimal.ZERO); + } + + for (String userId : visibleUserIds) { + BigDecimal sumAmount = calculateRecursiveTradeSumOptimized(userId, childrenByUserId, userTradeMoney); + tradeMoneySumByUserId.put(userId, sumAmount); + } + } + } + + List> treeRoots = new ArrayList<>(); + for (Map.Entry> entry : nodeByUserId.entrySet()) { + String userId = entry.getKey(); + Map node = entry.getValue(); + + if (hasTradeTimeFilter) { + node.put("selfTradeMoney", userTradeMoney.getOrDefault(userId, BigDecimal.ZERO)); + node.put("tradeMoneySum", tradeMoneySumByUserId.getOrDefault(userId, BigDecimal.ZERO)); + } + + String parentUserId = parentByUserId.get(userId); + Map parentNode = parentUserId == null ? null : nodeByUserId.get(parentUserId); + if (parentNode == null) { + treeRoots.add(node); + continue; + } + @SuppressWarnings("unchecked") + List> children = (List>) parentNode.get("children"); + children.add(node); + } + sortDepartmentUserTreeByName(treeRoots); + List> treeData = skipFirstTreeLevel(treeRoots); + + result.put("success", true); + result.put("message", "查询成功"); + result.put("data", treeData); + result.put("total", countTreeNodes(treeData)); + result.put("tenantId", tenantIdTrim); + return result; + } catch (Exception e) { + result.put("success", false); + result.put("message", "查询树形结构异常:" + e.getMessage()); + return result; + } + } + private static final class OrderBuyStats { private String earliestBuyTime; private LocalDateTime earliestBuyDateTime; diff --git a/src/main/resources/mapper/LbOrderRowMapper.xml b/src/main/resources/mapper/LbOrderRowMapper.xml index 476bcea..656ffd8 100644 --- a/src/main/resources/mapper/LbOrderRowMapper.xml +++ b/src/main/resources/mapper/LbOrderRowMapper.xml @@ -85,4 +85,37 @@ ) + + + +