diff --git a/src/main/java/com/rj/controller/LbDailyUserTradeReportController.java b/src/main/java/com/rj/controller/LbDailyUserTradeReportController.java
index 5f80843..422ccfd 100644
--- a/src/main/java/com/rj/controller/LbDailyUserTradeReportController.java
+++ b/src/main/java/com/rj/controller/LbDailyUserTradeReportController.java
@@ -2,6 +2,7 @@ package com.rj.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.rj.entity.LbDailyUserTrade;
import com.rj.entity.LbDailyUserTradeReport;
import com.rj.service.ILbDailyUserTradeReportService;
import io.swagger.v3.oas.annotations.Operation;
@@ -224,6 +225,143 @@ public class LbDailyUserTradeReportController {
}
}
+ @GetMapping("/list/daily-user-trade/by-user-id-and-date-range")
+ @Operation(summary = "按用户ID和日期范围分页查询交易明细", description = "根据开始日期、结束日期和用户ID,从lb_daily_user_trade分页查询交易数据")
+ public ResponseEntity
*/
public interface ILbDailyUserTradeReportService extends IService {
+ Page pageDailyUserTradeByUserIdAndDateRange(
+ LocalDate startDate, LocalDate endDate, String userId, Integer current, Integer size);
+ Page pageDailyUserTradeByParentIdOfUserAndDateRange(
+ LocalDate startDate, LocalDate endDate, String userId, Integer current, Integer size);
+
Map calculateReportSumByDateAndTenant(LocalDate reportDate, String tenantId);
/**
diff --git a/src/main/java/com/rj/service/impl/LbDailyUserTradeReportServiceImpl.java b/src/main/java/com/rj/service/impl/LbDailyUserTradeReportServiceImpl.java
index 82bf2dd..227bee1 100644
--- a/src/main/java/com/rj/service/impl/LbDailyUserTradeReportServiceImpl.java
+++ b/src/main/java/com/rj/service/impl/LbDailyUserTradeReportServiceImpl.java
@@ -1,12 +1,17 @@
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;
@@ -32,11 +37,50 @@ import java.util.UUID;
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) {
@@ -84,21 +128,297 @@ public class LbDailyUserTradeReportServiceImpl extends ServiceImpl checkInfo = buildCrossDayCheckInfo(reportDate, tenantId, start, end, yestodayBuyAmt);
+
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);
+ List rows = list(query);
+ return rows.isEmpty() ? null : rows.get(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 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<>();
@@ -122,13 +442,19 @@ public class LbDailyUserTradeReportServiceImpl extends ServiceImpl 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);
@@ -156,14 +482,37 @@ public class LbDailyUserTradeReportServiceImpl extends ServiceImpl scopeUserIds = new HashSet<>(ownAmtByUserId.keySet());
- // 2) 再查 lb_department_user:同一租户下的上下级(parent_id 为父用户 user_id),用于递归下级汇总
+ // 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;
@@ -172,12 +521,37 @@ public class LbDailyUserTradeReportServiceImpl extends ServiceImpl 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()) {
@@ -185,12 +559,15 @@ public class LbDailyUserTradeReportServiceImpl extends ServiceImpl 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);
@@ -211,13 +588,39 @@ public class LbDailyUserTradeReportServiceImpl extends ServiceImpl>());
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", "查询成功");
- result.put("data", rows);
+ // data 改为树形,flatData 保留平铺,便于前端平滑改造。
+ result.put("data", treeRows);
+ result.put("flatData", rows);
result.put("total", rows.size());
result.put("startDate", startDate);
result.put("endDate", endDate);
@@ -236,7 +639,26 @@ public class LbDailyUserTradeReportServiceImpl extends ServiceImpl> 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,
@@ -244,10 +666,12 @@ public class LbDailyUserTradeReportServiceImpl extends ServiceImpl ownAmtByUserId,
Map memo,
Set stack) {
+ // 动态规划缓存:同一节点只算一次。
TradeAmtBundle cached = memo.get(parentUserId);
if (cached != null) {
return cached;
}
+ // 防止环形关系导致无限递归。
if (!stack.add(parentUserId)) {
return TradeAmtBundle.zeros();
}
@@ -267,7 +691,10 @@ public class LbDailyUserTradeReportServiceImpl extends ServiceImpl()
.ge(LbDailyUserTrade::getReportDate, startDate)
.le(LbDailyUserTrade::getReportDate, endDate)
+ .ge(LbDailyUserTrade::getDailyBuyAmt, 1)
.isNotNull(LbDailyUserTrade::getNickname)
.orderByDesc(LbDailyUserTrade::getUpdatedAt)
.orderByDesc(LbDailyUserTrade::getCreatedAt)