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> listDailyUserTradeByUserIdAndDateRange( + @Parameter(description = "页码", example = "1") @RequestParam(defaultValue = "1") Integer current, + @Parameter(description = "每页大小", example = "10") @RequestParam(defaultValue = "10") Integer size, + @Parameter(description = "开始日期,格式:yyyy-MM-dd", required = true) @RequestParam String startDate, + @Parameter(description = "结束日期,格式:yyyy-MM-dd", required = true) @RequestParam String endDate, + @Parameter(description = "用户ID", required = true) @RequestParam String userId) { + Map result = new HashMap<>(); + try { + if (userId == null || userId.trim().isEmpty()) { + result.put("success", false); + result.put("message", "userId不能为空"); + return ResponseEntity.badRequest().body(result); + } + if (startDate == null || startDate.trim().isEmpty()) { + result.put("success", false); + result.put("message", "startDate不能为空"); + return ResponseEntity.badRequest().body(result); + } + if (endDate == null || endDate.trim().isEmpty()) { + result.put("success", false); + result.put("message", "endDate不能为空"); + return ResponseEntity.badRequest().body(result); + } + + LocalDate parsedStartDate; + LocalDate parsedEndDate; + try { + parsedStartDate = LocalDate.parse(startDate.trim()); + } catch (Exception e) { + result.put("success", false); + result.put("message", "startDate格式错误,请使用 yyyy-MM-dd"); + return ResponseEntity.badRequest().body(result); + } + try { + parsedEndDate = LocalDate.parse(endDate.trim()); + } catch (Exception e) { + result.put("success", false); + result.put("message", "endDate格式错误,请使用 yyyy-MM-dd"); + return ResponseEntity.badRequest().body(result); + } + if (parsedEndDate.isBefore(parsedStartDate)) { + result.put("success", false); + result.put("message", "endDate不能早于startDate"); + return ResponseEntity.badRequest().body(result); + } + + Page page = lbDailyUserTradeReportService.pageDailyUserTradeByUserIdAndDateRange( + parsedStartDate, parsedEndDate, userId.trim(), current, size); + + result.put("success", true); + result.put("message", "查询成功"); + result.put("data", page.getRecords()); + result.put("total", page.getTotal()); + result.put("current", page.getCurrent()); + result.put("size", page.getSize()); + result.put("pages", page.getPages()); + return ResponseEntity.ok(result); + } catch (Exception e) { + result.put("success", false); + result.put("message", "查询异常:" + e.getMessage()); + return ResponseEntity.internalServerError().body(result); + } + } + + @GetMapping("/list/daily-user-trade/by-parent-id-of-user-and-date-range") + @Operation(summary = "按用户ID和日期范围分页查询父节点的交易明细", + description = "根据用户ID查询lb_department_user的parent_id,再按user_id=parent_id从lb_daily_user_trade分页查询指定日期范围交易数据") + public ResponseEntity> listDailyUserTradeByParentIdOfUserAndDateRange( + @Parameter(description = "页码", example = "1") @RequestParam(defaultValue = "1") Integer current, + @Parameter(description = "每页大小", example = "10") @RequestParam(defaultValue = "10") Integer size, + @Parameter(description = "开始日期,格式:yyyy-MM-dd", required = true) @RequestParam String startDate, + @Parameter(description = "结束日期,格式:yyyy-MM-dd", required = true) @RequestParam String endDate, + @Parameter(description = "用户ID", required = true) @RequestParam String userId) { + Map result = new HashMap<>(); + try { + if (userId == null || userId.trim().isEmpty()) { + result.put("success", false); + result.put("message", "userId不能为空"); + return ResponseEntity.badRequest().body(result); + } + if (startDate == null || startDate.trim().isEmpty()) { + result.put("success", false); + result.put("message", "startDate不能为空"); + return ResponseEntity.badRequest().body(result); + } + if (endDate == null || endDate.trim().isEmpty()) { + result.put("success", false); + result.put("message", "endDate不能为空"); + return ResponseEntity.badRequest().body(result); + } + + LocalDate parsedStartDate; + LocalDate parsedEndDate; + try { + parsedStartDate = LocalDate.parse(startDate.trim()); + } catch (Exception e) { + result.put("success", false); + result.put("message", "startDate格式错误,请使用 yyyy-MM-dd"); + return ResponseEntity.badRequest().body(result); + } + try { + parsedEndDate = LocalDate.parse(endDate.trim()); + } catch (Exception e) { + result.put("success", false); + result.put("message", "endDate格式错误,请使用 yyyy-MM-dd"); + return ResponseEntity.badRequest().body(result); + } + if (parsedEndDate.isBefore(parsedStartDate)) { + result.put("success", false); + result.put("message", "endDate不能早于startDate"); + return ResponseEntity.badRequest().body(result); + } + + Page page = lbDailyUserTradeReportService.pageDailyUserTradeByParentIdOfUserAndDateRange( + parsedStartDate, parsedEndDate, userId.trim(), current, size); + + result.put("success", true); + result.put("message", "查询成功"); + result.put("data", page.getRecords()); + result.put("total", page.getTotal()); + result.put("current", page.getCurrent()); + result.put("size", page.getSize()); + result.put("pages", page.getPages()); + return ResponseEntity.ok(result); + } catch (IllegalArgumentException e) { + result.put("success", false); + result.put("message", e.getMessage()); + return ResponseEntity.badRequest().body(result); + } catch (Exception e) { + result.put("success", false); + result.put("message", "查询异常:" + e.getMessage()); + return ResponseEntity.internalServerError().body(result); + } + } + @PutMapping("/update") @Operation(summary = "修改报表记录", description = "根据ID修改当天用户交易报表记录") public ResponseEntity> update(@RequestBody LbDailyUserTradeReport tradeReport) { diff --git a/src/main/java/com/rj/service/ILbDailyUserTradeReportService.java b/src/main/java/com/rj/service/ILbDailyUserTradeReportService.java index f80a840..a8d4639 100644 --- a/src/main/java/com/rj/service/ILbDailyUserTradeReportService.java +++ b/src/main/java/com/rj/service/ILbDailyUserTradeReportService.java @@ -1,6 +1,8 @@ package com.rj.service; import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.rj.entity.LbDailyUserTrade; import com.rj.entity.LbDailyUserTradeReport; import java.time.LocalDate; @@ -12,6 +14,11 @@ import java.util.Map; *

*/ 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)