package com.rj.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.rj.entity.LbDailyUserTrade; import com.rj.entity.LbDailyUserTradeReport; import com.rj.entity.LbDepartmentUser; import com.rj.mapper.LbDailyUserTradeMapper; import com.rj.mapper.LbDailyUserTradeReportMapper; import com.rj.service.ILbDailyUserTradeReportService; import com.rj.service.ILbDepartmentUserService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; /** *

* 当天用户交易报表 服务实现类 *

*/ @Service public class LbDailyUserTradeReportServiceImpl extends ServiceImpl implements ILbDailyUserTradeReportService { private static final Logger log = LoggerFactory.getLogger(LbDailyUserTradeReportServiceImpl.class); private static final String DATA_TYPE_REPORT_SUM = "report_sum"; private static final String DATA_TYPE_REPORT_DETAIL = "report_detail"; private static final BigDecimal CROSS_DAY_DIFF_TOLERANCE = new BigDecimal("3"); @Autowired private ILbDepartmentUserService lbDepartmentUserService; @Autowired private LbDailyUserTradeMapper lbDailyUserTradeMapper; @Override public Page pageDailyUserTradeByUserIdAndDateRange( LocalDate startDate, LocalDate endDate, String userId, Integer current, Integer size) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(LbDailyUserTrade::getUserId, userId.trim()) .ge(LbDailyUserTrade::getReportDate, startDate) .le(LbDailyUserTrade::getReportDate, endDate) .orderByDesc(LbDailyUserTrade::getReportDate) .orderByDesc(LbDailyUserTrade::getCreatedAt); return lbDailyUserTradeMapper.selectPage(new Page<>(current, size), queryWrapper); } @Override public Page pageDailyUserTradeByParentIdOfUserAndDateRange( LocalDate startDate, LocalDate endDate, String userId, Integer current, Integer size) { LambdaQueryWrapper departmentUserQueryWrapper = new LambdaQueryWrapper<>(); departmentUserQueryWrapper.eq(LbDepartmentUser::getUserId, userId.trim()) .orderByDesc(LbDepartmentUser::getUpdateTime) .orderByDesc(LbDepartmentUser::getCreateTime) .last("limit 1"); LbDepartmentUser departmentUser = lbDepartmentUserService.getOne(departmentUserQueryWrapper, false); if (departmentUser == null || departmentUser.getParentId() == null || departmentUser.getParentId().trim().isEmpty()) { throw new IllegalArgumentException("未查询到该用户对应的parentId"); } String parentId = departmentUser.getParentId().trim(); LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(LbDailyUserTrade::getUserId, parentId) .ge(LbDailyUserTrade::getReportDate, startDate) .le(LbDailyUserTrade::getReportDate, endDate) .orderByDesc(LbDailyUserTrade::getReportDate) .orderByDesc(LbDailyUserTrade::getCreatedAt); return lbDailyUserTradeMapper.selectPage(new Page<>(current, size), queryWrapper); } @Override public Map calculateReportSumByDateAndTenant(LocalDate reportDate, String tenantId) { Map result = new HashMap<>(); LocalDateTime start = reportDate.atStartOfDay(); LocalDateTime end = reportDate.plusDays(1).atStartOfDay(); LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(LbDailyUserTradeReport::getTenantId, tenantId) .ge(LbDailyUserTradeReport::getReportDate, start) .lt(LbDailyUserTradeReport::getReportDate, end) .eq(LbDailyUserTradeReport::getDataType, DATA_TYPE_REPORT_DETAIL); BigDecimal yestodayBuyAmt = BigDecimal.ZERO; BigDecimal dailySellAmt = BigDecimal.ZERO; BigDecimal dailyBuyAmt = BigDecimal.ZERO; BigDecimal serviceAmt = BigDecimal.ZERO; BigDecimal diffAmt = BigDecimal.ZERO; BigDecimal dikouAmt = BigDecimal.ZERO; for (LbDailyUserTradeReport item : list(queryWrapper)) { yestodayBuyAmt = yestodayBuyAmt.add(defaultZero(item.getYestodayBuyAmt())); dailySellAmt = dailySellAmt.add(defaultZero(item.getDailySellAmt())); dailyBuyAmt = dailyBuyAmt.add(defaultZero(item.getDailyBuyAmt())); serviceAmt = serviceAmt.add(defaultZero(item.getServiceAmt())); diffAmt = diffAmt.add(defaultZero(item.getDiffAmt())); dikouAmt = dikouAmt.add(defaultZero(item.getDikouAmt())); } LbDailyUserTradeReport summary = new LbDailyUserTradeReport(); summary.setId(UUID.randomUUID().toString()); summary.setTenantId(tenantId); summary.setDataType(DATA_TYPE_REPORT_SUM); summary.setReportDate(start); summary.setYestodayBuyAmt(yestodayBuyAmt); summary.setDailySellAmt(dailySellAmt); summary.setDailyBuyAmt(dailyBuyAmt); summary.setServiceAmt(serviceAmt); summary.setDiffAmt(diffAmt); summary.setDikouAmt(dikouAmt); summary.setActualReceiptsPayments(diffAmt.subtract(serviceAmt)); summary.setDescContent("系统汇总记录"); summary.setCreatedAt(LocalDateTime.now()); summary.setUpdatedAt(LocalDateTime.now()); // 同日重复汇总时先清理历史汇总,避免 report_sum 产生多条导致读取口径不稳定。 LambdaQueryWrapper sumDeleteWrapper = new LambdaQueryWrapper<>(); sumDeleteWrapper.eq(LbDailyUserTradeReport::getTenantId, tenantId) .ge(LbDailyUserTradeReport::getReportDate, start) .lt(LbDailyUserTradeReport::getReportDate, end) .eq(LbDailyUserTradeReport::getDataType, DATA_TYPE_REPORT_SUM); remove(sumDeleteWrapper); boolean success = save(summary); Map checkInfo = buildCrossDayCheckInfo(reportDate, tenantId, start, end, yestodayBuyAmt); if (success && refreshSummaryFromDetail(summary, tenantId, start, end)) { checkInfo.put("summaryRefreshed", true); } result.put("success", success); if (success) { result.put("message", "汇总成功"); result.put("data", summary); result.put("checkInfo", checkInfo); return result; } result.put("message", "汇总失败"); return result; } private Map buildCrossDayCheckInfo( LocalDate reportDate, String tenantId, LocalDateTime todayStart, LocalDateTime todayEnd, BigDecimal todayYestodayBuyAmt) { Map checkInfo = new LinkedHashMap<>(); LocalDate yesterdayDate = reportDate.minusDays(1); LocalDateTime yesterdayStart = yesterdayDate.atStartOfDay(); LocalDateTime yesterdayEnd = reportDate.atStartOfDay(); BigDecimal tolerance = CROSS_DAY_DIFF_TOLERANCE; log.info("cross-day check started, tenantId={}, reportDate={}, yesterdayRange=[{},{}), todayRange=[{},{})", tenantId, reportDate, yesterdayStart, yesterdayEnd, todayStart, todayEnd); LbDailyUserTradeReport yesterdaySum = queryYesterdaySummary(tenantId, yesterdayStart, yesterdayEnd); if (yesterdaySum == null) { log.warn("cross-day check skipped: yesterday summary not found, tenantId={}, reportDate={}", tenantId, reportDate); checkInfo.put("yesterdaySumFound", false); checkInfo.put("needDetailCheck", false); checkInfo.put("detailCheckPassed", false); return checkInfo; } BigDecimal yesterdaySumBuyTotal = defaultZero(yesterdaySum.getDailyBuyAmt()); BigDecimal sumDiff = yesterdaySumBuyTotal.subtract(defaultZero(todayYestodayBuyAmt)).abs(); checkInfo.put("yesterdaySumFound", true); checkInfo.put("yesterdaySumBuyTotal", yesterdaySumBuyTotal); checkInfo.put("todayYestodayBuyAmt", todayYestodayBuyAmt); checkInfo.put("sumDiff", sumDiff); checkInfo.put("threshold", tolerance); log.info("cross-day summary compared, tenantId={}, reportDate={}, yesterdaySumBuyTotal={}, todayYestodayBuyAmt={}, diff={}, threshold={}", tenantId, reportDate, yesterdaySumBuyTotal, todayYestodayBuyAmt, sumDiff, tolerance); if (sumDiff.compareTo(tolerance) <= 0) { log.info("cross-day summary within threshold, skip detail check, tenantId={}, reportDate={}", tenantId, reportDate); checkInfo.put("needDetailCheck", false); checkInfo.put("detailCheckPassed", true); return checkInfo; } log.warn("cross-day summary exceeded threshold, start detail repair/check, tenantId={}, reportDate={}, diff={}", tenantId, reportDate, sumDiff); List yesterdayDetailList = queryDetailRows(tenantId, yesterdayStart, yesterdayEnd); List todayDetailList = queryDetailRows(tenantId, todayStart, todayEnd); log.info("detail rows loaded, tenantId={}, yesterdayCount={}, todayCount={}", tenantId, yesterdayDetailList.size(), todayDetailList.size()); Map yesterdayAmtByUserId = buildAmountByUserId(yesterdayDetailList, true); DetailRepairStat repairStat = repairTodayDetailByYesterday(yesterdayDetailList, todayDetailList, tenantId, todayStart); checkInfo.put("autoRepaired", repairStat.repaired); checkInfo.put("updatedTodayRows", repairStat.updatedCount); checkInfo.put("insertedTodayRows", repairStat.insertedCount); log.info("detail repair finished, tenantId={}, repaired={}, updatedRows={}, insertedRows={}", tenantId, repairStat.repaired, repairStat.updatedCount, repairStat.insertedCount); // 修正后重新读取今天明细并复核结果。 List repairedTodayDetailList = queryDetailRows(tenantId, todayStart, todayEnd); Map repairedTodayAmtByUserId = buildAmountByUserId(repairedTodayDetailList, false); fillDetailCheckResult(checkInfo, yesterdayAmtByUserId, repairedTodayAmtByUserId, yesterdayDetailList.size(), repairedTodayDetailList.size()); @SuppressWarnings("unchecked") List> mismatchUsers = (List>) checkInfo.get("mismatchUsers"); @SuppressWarnings("unchecked") List missingInTodayUsers = (List) checkInfo.get("missingInTodayUsers"); @SuppressWarnings("unchecked") List extraInTodayUsers = (List) checkInfo.get("extraInTodayUsers"); if (!mismatchUsers.isEmpty() || !missingInTodayUsers.isEmpty() || !extraInTodayUsers.isEmpty()) { log.warn("trade report detail check failed, tenantId={}, reportDate={}, sumDiff={}, mismatchCount={}, missingCount={}, extraCount={}", tenantId, reportDate, sumDiff, mismatchUsers.size(), missingInTodayUsers.size(), extraInTodayUsers.size()); } else { log.info("trade report detail check passed after repair, tenantId={}, reportDate={}, sumDiff={}", tenantId, reportDate, sumDiff); } return checkInfo; } private LbDailyUserTradeReport queryYesterdaySummary(String tenantId, LocalDateTime yesterdayStart, LocalDateTime yesterdayEnd) { LambdaQueryWrapper query = new LambdaQueryWrapper<>(); query.eq(LbDailyUserTradeReport::getTenantId, tenantId) .ge(LbDailyUserTradeReport::getReportDate, yesterdayStart) .lt(LbDailyUserTradeReport::getReportDate, yesterdayEnd) .eq(LbDailyUserTradeReport::getDataType, DATA_TYPE_REPORT_SUM) .orderByDesc(LbDailyUserTradeReport::getUpdatedAt) .orderByDesc(LbDailyUserTradeReport::getCreatedAt) .last("limit 1"); List rows = list(query); return rows.isEmpty() ? null : rows.get(0); } private boolean refreshSummaryFromDetail( LbDailyUserTradeReport summary, String tenantId, LocalDateTime start, LocalDateTime end) { TradeSummaryAmount latest = aggregateDetailAmounts(tenantId, start, end); boolean changed = isSummaryChanged(summary, latest); if (!changed) { return false; } summary.setYestodayBuyAmt(latest.yestodayBuyAmt); summary.setDailySellAmt(latest.dailySellAmt); summary.setDailyBuyAmt(latest.dailyBuyAmt); summary.setServiceAmt(latest.serviceAmt); summary.setDiffAmt(latest.diffAmt); summary.setDikouAmt(latest.dikouAmt); summary.setActualReceiptsPayments(latest.diffAmt.subtract(latest.serviceAmt)); summary.setUpdatedAt(LocalDateTime.now()); updateById(summary); return true; } private TradeSummaryAmount aggregateDetailAmounts(String tenantId, LocalDateTime start, LocalDateTime end) { LambdaQueryWrapper detailQuery = new LambdaQueryWrapper<>(); detailQuery.eq(LbDailyUserTradeReport::getTenantId, tenantId) .ge(LbDailyUserTradeReport::getReportDate, start) .lt(LbDailyUserTradeReport::getReportDate, end) .eq(LbDailyUserTradeReport::getDataType, DATA_TYPE_REPORT_DETAIL); TradeSummaryAmount amount = new TradeSummaryAmount(); for (LbDailyUserTradeReport row : list(detailQuery)) { amount.yestodayBuyAmt = amount.yestodayBuyAmt.add(defaultZero(row.getYestodayBuyAmt())); amount.dailySellAmt = amount.dailySellAmt.add(defaultZero(row.getDailySellAmt())); amount.dailyBuyAmt = amount.dailyBuyAmt.add(defaultZero(row.getDailyBuyAmt())); amount.serviceAmt = amount.serviceAmt.add(defaultZero(row.getServiceAmt())); amount.diffAmt = amount.diffAmt.add(defaultZero(row.getDiffAmt())); amount.dikouAmt = amount.dikouAmt.add(defaultZero(row.getDikouAmt())); } return amount; } private boolean isSummaryChanged(LbDailyUserTradeReport summary, TradeSummaryAmount latest) { return defaultZero(summary.getYestodayBuyAmt()).compareTo(latest.yestodayBuyAmt) != 0 || defaultZero(summary.getDailySellAmt()).compareTo(latest.dailySellAmt) != 0 || defaultZero(summary.getDailyBuyAmt()).compareTo(latest.dailyBuyAmt) != 0 || defaultZero(summary.getServiceAmt()).compareTo(latest.serviceAmt) != 0 || defaultZero(summary.getDiffAmt()).compareTo(latest.diffAmt) != 0 || defaultZero(summary.getDikouAmt()).compareTo(latest.dikouAmt) != 0; } private List queryDetailRows(String tenantId, LocalDateTime start, LocalDateTime end) { LambdaQueryWrapper query = new LambdaQueryWrapper<>(); query.eq(LbDailyUserTradeReport::getTenantId, tenantId) .ge(LbDailyUserTradeReport::getReportDate, start) .lt(LbDailyUserTradeReport::getReportDate, end) .eq(LbDailyUserTradeReport::getDataType, DATA_TYPE_REPORT_DETAIL); return list(query); } private Map buildAmountByUserId(List detailRows, boolean useDailyBuyAmt) { Map amountByUserId = new HashMap<>(); for (LbDailyUserTradeReport row : detailRows) { if (row.getUserId() == null || row.getUserId().trim().isEmpty()) { continue; } String userId = row.getUserId().trim(); BigDecimal amount = useDailyBuyAmt ? defaultZero(row.getDailyBuyAmt()) : defaultZero(row.getYestodayBuyAmt()); amountByUserId.merge(userId, amount, BigDecimal::add); } return amountByUserId; } private DetailRepairStat repairTodayDetailByYesterday( List yesterdayDetailList, List todayDetailList, String tenantId, LocalDateTime todayStart) { // Step1: 先把“昨天每个用户应有金额”聚合好,作为今天修正目标。 Map yesterdayAmtByUserId = new HashMap<>(); Map yesterdaySampleByUserId = new HashMap<>(); for (LbDailyUserTradeReport row : yesterdayDetailList) { if (row.getUserId() == null || row.getUserId().trim().isEmpty()) { continue; } String userId = row.getUserId().trim(); yesterdayAmtByUserId.merge(userId, defaultZero(row.getDailyBuyAmt()), BigDecimal::add); yesterdaySampleByUserId.putIfAbsent(userId, row); } // Step2: 对今天明细建立 userId -> rows 映射,便于 O(1) 找到待修正记录。 Map> todayRowsByUserId = new HashMap<>(); for (LbDailyUserTradeReport row : todayDetailList) { if (row.getUserId() == null || row.getUserId().trim().isEmpty()) { continue; } String userId = row.getUserId().trim(); todayRowsByUserId.computeIfAbsent(userId, k -> new ArrayList<>()).add(row); } List rowsToUpdate = new ArrayList<>(); List rowsToInsert = new ArrayList<>(); LocalDateTime now = LocalDateTime.now(); int duplicateRowZeroedCount = 0; // Step3: 以“昨天聚合金额”为唯一标准回写今天数据。 for (Map.Entry entry : yesterdayAmtByUserId.entrySet()) { String userId = entry.getKey(); BigDecimal expectedAmt = defaultZero(entry.getValue()); List todayRows = todayRowsByUserId.get(userId); if (todayRows == null || todayRows.isEmpty()) { LbDailyUserTradeReport source = yesterdaySampleByUserId.get(userId); LbDailyUserTradeReport inserted = new LbDailyUserTradeReport(); inserted.setId(UUID.randomUUID().toString()); inserted.setTenantId(tenantId); inserted.setUserId(userId); inserted.setNickname(source == null ? null : source.getNickname()); inserted.setDataType(DATA_TYPE_REPORT_DETAIL); inserted.setReportDate(todayStart); inserted.setYestodayBuyAmt(expectedAmt); inserted.setDailySellAmt(BigDecimal.ZERO); inserted.setDailyBuyAmt(BigDecimal.ZERO); inserted.setServiceAmt(BigDecimal.ZERO); inserted.setDiffAmt(BigDecimal.ZERO); inserted.setDikouAmt(BigDecimal.ZERO); inserted.setActualReceiptsPayments(BigDecimal.ZERO); inserted.setDescContent("系统按昨日数据自动修正"); inserted.setCreatedAt(now); inserted.setUpdatedAt(now); rowsToInsert.add(inserted); log.info("detail repair insert row, tenantId={}, userId={}, expectedAmt={}", tenantId, userId, expectedAmt); continue; } LbDailyUserTradeReport first = todayRows.get(0); if (defaultZero(first.getYestodayBuyAmt()).compareTo(expectedAmt) != 0) { first.setYestodayBuyAmt(expectedAmt); first.setUpdatedAt(now); rowsToUpdate.add(first); log.info("detail repair update row, tenantId={}, userId={}, rowId={}, expectedAmt={}", tenantId, userId, first.getId(), expectedAmt); } for (int i = 1; i < todayRows.size(); i++) { LbDailyUserTradeReport extra = todayRows.get(i); if (defaultZero(extra.getYestodayBuyAmt()).compareTo(BigDecimal.ZERO) != 0) { extra.setYestodayBuyAmt(BigDecimal.ZERO); extra.setUpdatedAt(now); rowsToUpdate.add(extra); duplicateRowZeroedCount++; } } } boolean repaired = false; if (!rowsToUpdate.isEmpty()) { saveOrUpdateBatch(rowsToUpdate); repaired = true; } if (!rowsToInsert.isEmpty()) { saveBatch(rowsToInsert); repaired = true; } log.info("detail repair summary, tenantId={}, targetUserCount={}, updatedRows={}, insertedRows={}, duplicateRowsZeroed={}", tenantId, yesterdayAmtByUserId.size(), rowsToUpdate.size(), rowsToInsert.size(), duplicateRowZeroedCount); return new DetailRepairStat(repaired, rowsToUpdate.size(), rowsToInsert.size()); } private void fillDetailCheckResult( Map checkInfo, Map yesterdayAmtByUserId, Map todayAmtByUserId, int yesterdayDetailCount, int todayDetailCount) { List> mismatchUsers = new ArrayList<>(); List missingInTodayUsers = new ArrayList<>(); List extraInTodayUsers = new ArrayList<>(); Set allUserIds = new HashSet<>(yesterdayAmtByUserId.keySet()); allUserIds.addAll(todayAmtByUserId.keySet()); for (String userId : allUserIds) { BigDecimal yesterdayUserAmt = yesterdayAmtByUserId.get(userId); BigDecimal todayUserAmt = todayAmtByUserId.get(userId); if (yesterdayUserAmt == null && todayUserAmt != null) { extraInTodayUsers.add(userId); continue; } if (yesterdayUserAmt != null && todayUserAmt == null) { missingInTodayUsers.add(userId); continue; } BigDecimal userDiff = defaultZero(yesterdayUserAmt).subtract(defaultZero(todayUserAmt)).abs(); if (userDiff.compareTo(BigDecimal.ZERO) > 0) { Map mismatch = new LinkedHashMap<>(); mismatch.put("userId", userId); mismatch.put("yesterdayAmt", yesterdayUserAmt); mismatch.put("todayAmt", todayUserAmt); mismatch.put("diff", userDiff); mismatchUsers.add(mismatch); } } checkInfo.put("needDetailCheck", true); checkInfo.put("yesterdayDetailCount", yesterdayDetailCount); checkInfo.put("todayDetailCount", todayDetailCount); checkInfo.put("mismatchUsers", mismatchUsers); checkInfo.put("missingInTodayUsers", missingInTodayUsers); checkInfo.put("extraInTodayUsers", extraInTodayUsers); checkInfo.put("detailCheckPassed", mismatchUsers.isEmpty() && missingInTodayUsers.isEmpty() && extraInTodayUsers.isEmpty()); } private static final class DetailRepairStat { final boolean repaired; final int updatedCount; final int insertedCount; DetailRepairStat(boolean repaired, int updatedCount, int insertedCount) { this.repaired = repaired; this.updatedCount = updatedCount; this.insertedCount = insertedCount; } } private static final class TradeSummaryAmount { BigDecimal yestodayBuyAmt = BigDecimal.ZERO; BigDecimal dailySellAmt = BigDecimal.ZERO; BigDecimal dailyBuyAmt = BigDecimal.ZERO; BigDecimal serviceAmt = BigDecimal.ZERO; BigDecimal diffAmt = BigDecimal.ZERO; BigDecimal dikouAmt = BigDecimal.ZERO; } private static BigDecimal defaultZero(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } @Override /** * 按日期范围统计“本人 + 递归团队”的交易金额。 * * 设计说明: * 1) 先用交易报表圈定“本次需要返回”的用户集合(scopeUserIds),并聚合每个用户 own 金额; * 2) 再用部门关系构建 parentUserId -> childrenUserIds 的树; * 3) 对 scope 中每个用户递归汇总 team 金额(不含本人); * 4) 返回 own/team/total 三套字段,便于前端直接展示。 * * 注意: * - 交易报表必须带 tenant 过滤,否则会混入其他租户用户导致层级结果异常; * - lb_department_user.parent_id 在历史数据中可能是“父user_id”或“父记录id(UUID)”,此处做兼容解析。 */ public Map listUserTradeAmountWithTeamByDateRangeAndTenant( LocalDate startDate, LocalDate endDate, String tenantId) { Map result = new HashMap<>(); if (tenantId == null || tenantId.trim().isEmpty()) { result.put("success", false); result.put("message", "tenantId不能为空"); return result; } if (startDate == null || endDate == null) { result.put("success", false); result.put("message", "开始日期和结束日期不能为空"); return result; } if (endDate.isBefore(startDate)) { result.put("success", false); result.put("message", "结束日期不能早于开始日期"); return result; } String tenantIdTrim = tenantId.trim(); LocalDateTime rangeStart = startDate.atStartOfDay(); LocalDateTime rangeEnd = endDate.atTime(23, 59, 59); // 1) 先查 lb_daily_user_trade_report: // - 仅取当前租户 + 指定日期范围 + report_detail; // - 用于圈定返回范围(scopeUserIds); // - 同时把本人四类金额先聚合好(ownAmtByUserId)。 LambdaQueryWrapper tradeQw = new LambdaQueryWrapper<>(); tradeQw.ge(LbDailyUserTradeReport::getReportDate, rangeStart) .le(LbDailyUserTradeReport::getReportDate, rangeEnd) .eq(LbDailyUserTradeReport::getTenantId, tenantIdTrim) .eq(LbDailyUserTradeReport::getDataType, DATA_TYPE_REPORT_DETAIL ); // userId -> 姓名,优先用报表昵称,后续再用部门姓名补齐。 Map userIdToName = new HashMap<>(); // userId -> 本人金额汇总(own)。 Map ownAmtByUserId = new HashMap<>(); List list1 = list(tradeQw); for (LbDailyUserTradeReport r : list1) { if (r.getUserId() == null || r.getUserId().trim().isEmpty()) { continue; } String uid = r.getUserId().trim(); ownAmtByUserId .computeIfAbsent(uid, k -> TradeAmtBundle.zeros()) .addFromReportRow(r); if (r.getNickname() != null && !r.getNickname().trim().isEmpty()) { userIdToName.putIfAbsent(uid, r.getNickname().trim()); } } if (ownAmtByUserId.isEmpty()) { result.put("success", true); result.put("message", "查询成功"); result.put("data", List.of()); result.put("total", 0); result.put("startDate", startDate); result.put("endDate", endDate); result.put("tenantId", tenantIdTrim); return result; } // 最终只返回这些用户(报表范围内“有交易数据”的用户)。 Set scopeUserIds = new HashSet<>(ownAmtByUserId.keySet()); // 2) 再查 lb_department_user,构建上下级关系: // 目标结构:childrenByParentUserId[parentUserId] = [childUserId...] // 兼容 parent_id 两种格式: // - 直接存父 user_id; // - 存父记录 id(UUID),需先反查成父 user_id。 Map> childrenByParentUserId = new HashMap<>(); // childUserId -> parentUserId,用于后续把平铺数据组装成树。 Map parentByUserId = new HashMap<>(); List deptUsers = lbDepartmentUserService.list( new LambdaQueryWrapper() .eq(LbDepartmentUser::getTenantId, tenantIdTrim)); if (deptUsers != null) { // 预处理索引: // - userIdByRecordId: 记录id(UUID) -> userId // - allUserIds: 全部 userId(用于判断 parent_id 是否已是 userId) Map userIdByRecordId = new HashMap<>(); Set allUserIds = new HashSet<>(); for (LbDepartmentUser row : deptUsers) { String rowId = row.getId() == null ? null : row.getId().trim(); String uid = row.getUserId() == null ? null : row.getUserId().trim(); if (uid == null || uid.isEmpty()) { continue; } allUserIds.add(uid); if (rowId != null && !rowId.isEmpty()) { userIdByRecordId.put(rowId, uid); } } for (LbDepartmentUser row : deptUsers) { if (row.getUserId() == null || row.getUserId().trim().isEmpty()) { continue; } String uid = row.getUserId().trim(); if (row.getName() != null && !row.getName().trim().isEmpty()) { userIdToName.putIfAbsent(uid, row.getName().trim()); } // parentRef 可能是“父userId”或“父记录id”,不要直接假设其一定是 userId。 String parentRef = row.getParentId() == null ? null : row.getParentId().trim(); if (parentRef != null && !parentRef.isEmpty()) { String parentUserId = null; String resolvedBy = null; // case A:parent_id 本身就是父 userId if (allUserIds.contains(parentRef)) { parentUserId = parentRef; resolvedBy = "parent_user_id"; // case B:parent_id 是父记录id(UUID),反查映射到父 userId } else if (userIdByRecordId.containsKey(parentRef)) { parentUserId = userIdByRecordId.get(parentRef); resolvedBy = "parent_record_id"; } // 两种都解析失败:说明关系数据不完整/不一致,记录日志便于排查。 if (parentUserId == null || parentUserId.isEmpty()) { log.warn("dept relation parent unresolved, tenantId={}, userId={}, name={}, parentId={}", tenantIdTrim, uid, row.getName(), parentRef); continue; } List ch = childrenByParentUserId.computeIfAbsent(parentUserId, k -> new ArrayList<>()); if (!ch.contains(uid)) { ch.add(uid); } // 若同一个子节点出现多次,保留第一次解析到的父节点,避免树结构抖动。 parentByUserId.putIfAbsent(uid, parentUserId); // 针对已知问题用户打定位日志,便于验证 parent_id 解析是否正确。 if ("98155".equals(uid) || "杨渺渺".equals(row.getName())) { log.info("dept relation resolved for focus user, tenantId={}, userId={}, name={}, parentId={}, parentUserId={}, resolvedBy={}", tenantIdTrim, uid, row.getName(), parentRef, parentUserId, resolvedBy); } } } for (List list : childrenByParentUserId.values()) { list.sort(String::compareTo); } } // 3) 对 scope 用户计算递归 team 金额: // - team = 所有下级 own + 下级的 team(不含本人); // - 使用 memo 缓存避免重复递归。 Map teamAmtMemo = new HashMap<>(); for (String userId : scopeUserIds) { teamAmtRecursive(userId, childrenByParentUserId, ownAmtByUserId, teamAmtMemo, new HashSet<>()); } // 4) 先组装平铺结果,保持 own/team/total 三套字段并按 totalDiffAmt 倒序。 List> rows = new ArrayList<>(); for (String userId : scopeUserIds) { TradeAmtBundle own = ownAmt(userId, ownAmtByUserId); TradeAmtBundle team = teamAmt(userId, teamAmtMemo); TradeAmtBundle total = own.addCopy(team); Map row = new LinkedHashMap<>(); row.put("userId", userId); row.put("name", userIdToName.getOrDefault(userId, "")); row.put("ownYestodayBuyAmt", own.yestodayBuyAmt); row.put("teamYestodayBuyAmt", team.yestodayBuyAmt); row.put("totalYestodayBuyAmt", total.yestodayBuyAmt); row.put("ownDailySellAmt", own.dailySellAmt); row.put("teamDailySellAmt", team.dailySellAmt); row.put("totalDailySellAmt", total.dailySellAmt); row.put("ownDailyBuyAmt", own.dailyBuyAmt); row.put("teamDailyBuyAmt", team.dailyBuyAmt); row.put("totalDailyBuyAmt", total.dailyBuyAmt); row.put("ownDiffAmt", own.diffAmt); row.put("teamDiffAmt", team.diffAmt); row.put("totalDiffAmt", total.diffAmt); // children 用于前端树形展示。 row.put("children", new ArrayList>()); rows.add(row); } rows.sort(Comparator.comparing((Map m) -> (BigDecimal) m.get("totalDiffAmt")).reversed()); // 5) 将平铺数据转为树形结构: // - 仅挂载 scope(报表内有交易数据)节点,避免返回无交易的空节点; // - 若父节点不在 scope,则当前节点作为根节点返回。 Map> rowByUserId = new HashMap<>(); for (Map row : rows) { rowByUserId.put((String) row.get("userId"), row); } List> treeRows = new ArrayList<>(); for (Map row : rows) { String userId = (String) row.get("userId"); String parentUserId = parentByUserId.get(userId); Map parentRow = parentUserId == null ? null : rowByUserId.get(parentUserId); if (parentRow == null) { treeRows.add(row); continue; } @SuppressWarnings("unchecked") List> children = (List>) parentRow.get("children"); children.add(row); } sortTreeByTotalDiffDesc(treeRows); result.put("success", true); result.put("message", "查询成功"); // data 改为树形,flatData 保留平铺,便于前端平滑改造。 result.put("data", treeRows); result.put("flatData", rows); result.put("total", rows.size()); result.put("startDate", startDate); result.put("endDate", endDate); result.put("tenantId", tenantIdTrim); return result; } private static TradeAmtBundle ownAmt(String userId, Map ownAmtByUserId) { TradeAmtBundle b = ownAmtByUserId.get(userId); return b != null ? b : TradeAmtBundle.zeros(); } private static TradeAmtBundle teamAmt(String userId, Map teamAmtMemo) { TradeAmtBundle b = teamAmtMemo.get(userId); return b != null ? b : TradeAmtBundle.zeros(); } /** * 递归按 totalDiffAmt 倒序排序树节点,确保每层展示稳定。 */ private static void sortTreeByTotalDiffDesc(List> nodes) { nodes.sort(Comparator.comparing((Map m) -> (BigDecimal) m.get("totalDiffAmt")).reversed()); for (Map node : nodes) { @SuppressWarnings("unchecked") List> children = (List>) node.get("children"); if (children != null && !children.isEmpty()) { sortTreeByTotalDiffDesc(children); } } } /** * 递归计算某个用户的 team 金额(不含本人)。 * * 规则: * - sum = Σ(每个child的own + child的team); * - memo 命中即直接返回,避免重复计算; * - stack 用于防环(脏数据导致 A->B->A 时返回 0,避免死循环)。 */ private TradeAmtBundle teamAmtRecursive( String parentUserId, Map> childrenByParentUserId, Map ownAmtByUserId, Map memo, Set stack) { // 动态规划缓存:同一节点只算一次。 TradeAmtBundle cached = memo.get(parentUserId); if (cached != null) { return cached; } // 防止环形关系导致无限递归。 if (!stack.add(parentUserId)) { return TradeAmtBundle.zeros(); } try { TradeAmtBundle sum = TradeAmtBundle.zeros(); List children = childrenByParentUserId.get(parentUserId); if (children != null) { for (String childUserId : children) { sum.addInPlace(ownAmt(childUserId, ownAmtByUserId)); sum.addInPlace(teamAmtRecursive(childUserId, childrenByParentUserId, ownAmtByUserId, memo, stack)); } } memo.put(parentUserId, sum); return sum; } finally { stack.remove(parentUserId); } } /** * 日期范围内按用户汇总的四类金额(与报表字段对应)。 * 该对象既可表示 own,也可表示 team 或 total。 */ private static final class TradeAmtBundle { BigDecimal yestodayBuyAmt = BigDecimal.ZERO; BigDecimal dailySellAmt = BigDecimal.ZERO; BigDecimal dailyBuyAmt = BigDecimal.ZERO; BigDecimal diffAmt = BigDecimal.ZERO; static TradeAmtBundle zeros() { return new TradeAmtBundle(); } void addFromReportRow(LbDailyUserTradeReport r) { yestodayBuyAmt = yestodayBuyAmt.add(defaultZero(r.getYestodayBuyAmt())); dailySellAmt = dailySellAmt.add(defaultZero(r.getDailySellAmt())); dailyBuyAmt = dailyBuyAmt.add(defaultZero(r.getDailyBuyAmt())); diffAmt = diffAmt.add(defaultZero(r.getDiffAmt())); } void addInPlace(TradeAmtBundle o) { if (o == null) { return; } yestodayBuyAmt = yestodayBuyAmt.add(defaultZero(o.yestodayBuyAmt)); dailySellAmt = dailySellAmt.add(defaultZero(o.dailySellAmt)); dailyBuyAmt = dailyBuyAmt.add(defaultZero(o.dailyBuyAmt)); diffAmt = diffAmt.add(defaultZero(o.diffAmt)); } TradeAmtBundle addCopy(TradeAmtBundle o) { TradeAmtBundle n = zeros(); n.addInPlace(this); n.addInPlace(o != null ? o : zeros()); return n; } } }