给树形结构添加查询交易明细的功能

This commit is contained in:
2026-04-30 20:25:13 +08:00
parent cfec435e87
commit ecb4b6dc26
4 changed files with 581 additions and 8 deletions

View File

@@ -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<Map<String, Object>> 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<String, Object> 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<LbDailyUserTrade> 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<Map<String, Object>> 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<String, Object> 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<LbDailyUserTrade> 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<Map<String, Object>> update(@RequestBody LbDailyUserTradeReport tradeReport) {

View File

@@ -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;
* </p>
*/
public interface ILbDailyUserTradeReportService extends IService<LbDailyUserTradeReport> {
Page<LbDailyUserTrade> pageDailyUserTradeByUserIdAndDateRange(
LocalDate startDate, LocalDate endDate, String userId, Integer current, Integer size);
Page<LbDailyUserTrade> pageDailyUserTradeByParentIdOfUserAndDateRange(
LocalDate startDate, LocalDate endDate, String userId, Integer current, Integer size);
Map<String, Object> calculateReportSumByDateAndTenant(LocalDate reportDate, String tenantId);
/**

View File

@@ -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<LbDailyUserTradeReportMapper, LbDailyUserTradeReport>
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<LbDailyUserTrade> pageDailyUserTradeByUserIdAndDateRange(
LocalDate startDate, LocalDate endDate, String userId, Integer current, Integer size) {
LambdaQueryWrapper<LbDailyUserTrade> 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<LbDailyUserTrade> pageDailyUserTradeByParentIdOfUserAndDateRange(
LocalDate startDate, LocalDate endDate, String userId, Integer current, Integer size) {
LambdaQueryWrapper<LbDepartmentUser> 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<LbDailyUserTrade> 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<String, Object> calculateReportSumByDateAndTenant(LocalDate reportDate, String tenantId) {
@@ -84,21 +128,297 @@ public class LbDailyUserTradeReportServiceImpl extends ServiceImpl<LbDailyUserTr
summary.setUpdatedAt(LocalDateTime.now());
boolean success = save(summary);
Map<String, Object> 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<String, Object> buildCrossDayCheckInfo(
LocalDate reportDate,
String tenantId,
LocalDateTime todayStart,
LocalDateTime todayEnd,
BigDecimal todayYestodayBuyAmt) {
Map<String, Object> 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<LbDailyUserTradeReport> yesterdayDetailList = queryDetailRows(tenantId, yesterdayStart, yesterdayEnd);
List<LbDailyUserTradeReport> todayDetailList = queryDetailRows(tenantId, todayStart, todayEnd);
log.info("detail rows loaded, tenantId={}, yesterdayCount={}, todayCount={}",
tenantId, yesterdayDetailList.size(), todayDetailList.size());
Map<String, BigDecimal> 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<LbDailyUserTradeReport> repairedTodayDetailList = queryDetailRows(tenantId, todayStart, todayEnd);
Map<String, BigDecimal> repairedTodayAmtByUserId = buildAmountByUserId(repairedTodayDetailList, false);
fillDetailCheckResult(checkInfo, yesterdayAmtByUserId, repairedTodayAmtByUserId, yesterdayDetailList.size(), repairedTodayDetailList.size());
@SuppressWarnings("unchecked")
List<Map<String, Object>> mismatchUsers = (List<Map<String, Object>>) checkInfo.get("mismatchUsers");
@SuppressWarnings("unchecked")
List<String> missingInTodayUsers = (List<String>) checkInfo.get("missingInTodayUsers");
@SuppressWarnings("unchecked")
List<String> extraInTodayUsers = (List<String>) 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<LbDailyUserTradeReport> query = new LambdaQueryWrapper<>();
query.eq(LbDailyUserTradeReport::getTenantId, tenantId)
.ge(LbDailyUserTradeReport::getReportDate, yesterdayStart)
.lt(LbDailyUserTradeReport::getReportDate, yesterdayEnd)
.eq(LbDailyUserTradeReport::getDataType, DATA_TYPE_REPORT_SUM);
List<LbDailyUserTradeReport> rows = list(query);
return rows.isEmpty() ? null : rows.get(0);
}
private List<LbDailyUserTradeReport> queryDetailRows(String tenantId, LocalDateTime start, LocalDateTime end) {
LambdaQueryWrapper<LbDailyUserTradeReport> 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<String, BigDecimal> buildAmountByUserId(List<LbDailyUserTradeReport> detailRows, boolean useDailyBuyAmt) {
Map<String, BigDecimal> 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<LbDailyUserTradeReport> yesterdayDetailList,
List<LbDailyUserTradeReport> todayDetailList,
String tenantId,
LocalDateTime todayStart) {
// Step1: 先把“昨天每个用户应有金额”聚合好,作为今天修正目标。
Map<String, BigDecimal> yesterdayAmtByUserId = new HashMap<>();
Map<String, LbDailyUserTradeReport> 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<String, List<LbDailyUserTradeReport>> 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<LbDailyUserTradeReport> rowsToUpdate = new ArrayList<>();
List<LbDailyUserTradeReport> rowsToInsert = new ArrayList<>();
LocalDateTime now = LocalDateTime.now();
int duplicateRowZeroedCount = 0;
// Step3: 以“昨天聚合金额”为唯一标准回写今天数据。
for (Map.Entry<String, BigDecimal> entry : yesterdayAmtByUserId.entrySet()) {
String userId = entry.getKey();
BigDecimal expectedAmt = defaultZero(entry.getValue());
List<LbDailyUserTradeReport> 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<String, Object> checkInfo,
Map<String, BigDecimal> yesterdayAmtByUserId,
Map<String, BigDecimal> todayAmtByUserId,
int yesterdayDetailCount,
int todayDetailCount) {
List<Map<String, Object>> mismatchUsers = new ArrayList<>();
List<String> missingInTodayUsers = new ArrayList<>();
List<String> extraInTodayUsers = new ArrayList<>();
Set<String> 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<String, Object> 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<String, Object> listUserTradeAmountWithTeamByDateRangeAndTenant(
LocalDate startDate, LocalDate endDate, String tenantId) {
Map<String, Object> result = new HashMap<>();
@@ -122,13 +442,19 @@ public class LbDailyUserTradeReportServiceImpl extends ServiceImpl<LbDailyUserTr
LocalDateTime rangeStart = startDate.atStartOfDay();
LocalDateTime rangeEnd = endDate.atTime(23, 59, 59);
// 1) 先查 lb_daily_user_trade_report圈定日期内有数据的用户,并汇总本人四类金额
// 1) 先查 lb_daily_user_trade_report
// - 仅取当前租户 + 指定日期范围 + report_detail
// - 用于圈定返回范围scopeUserIds
// - 同时把本人四类金额先聚合好ownAmtByUserId
LambdaQueryWrapper<LbDailyUserTradeReport> 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<String, String> userIdToName = new HashMap<>();
// userId -> 本人金额汇总own
Map<String, TradeAmtBundle> ownAmtByUserId = new HashMap<>();
List<LbDailyUserTradeReport> list1 = list(tradeQw);
@@ -156,14 +482,37 @@ public class LbDailyUserTradeReportServiceImpl extends ServiceImpl<LbDailyUserTr
return result;
}
// 最终只返回这些用户(报表范围内“有交易数据”的用户)。
Set<String> 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<String, List<String>> childrenByParentUserId = new HashMap<>();
// childUserId -> parentUserId用于后续把平铺数据组装成树。
Map<String, String> parentByUserId = new HashMap<>();
List<LbDepartmentUser> deptUsers = lbDepartmentUserService.list(
new LambdaQueryWrapper<LbDepartmentUser>()
.eq(LbDepartmentUser::getTenantId, tenantIdTrim));
if (deptUsers != null) {
// 预处理索引:
// - userIdByRecordId: 记录id(UUID) -> userId
// - allUserIds: 全部 userId用于判断 parent_id 是否已是 userId
Map<String, String> userIdByRecordId = new HashMap<>();
Set<String> 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<LbDailyUserTr
if (row.getName() != null && !row.getName().trim().isEmpty()) {
userIdToName.putIfAbsent(uid, row.getName().trim());
}
String parentUserId = row.getParentId() == null ? null : row.getParentId().trim();
if (parentUserId != null && !parentUserId.isEmpty()) {
// 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 Aparent_id 本身就是父 userId
if (allUserIds.contains(parentRef)) {
parentUserId = parentRef;
resolvedBy = "parent_user_id";
// case Bparent_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<String> 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<String> list : childrenByParentUserId.values()) {
@@ -185,12 +559,15 @@ public class LbDailyUserTradeReportServiceImpl extends ServiceImpl<LbDailyUserTr
}
}
// 3) 仅对报表圈定的用户计算递归下级 team(无部门数据时 team 全为 0
// 3) 对 scope 用户计算递归 team 金额:
// - team = 所有下级 own + 下级的 team不含本人
// - 使用 memo 缓存避免重复递归。
Map<String, TradeAmtBundle> teamAmtMemo = new HashMap<>();
for (String userId : scopeUserIds) {
teamAmtRecursive(userId, childrenByParentUserId, ownAmtByUserId, teamAmtMemo, new HashSet<>());
}
// 4) 先组装平铺结果,保持 own/team/total 三套字段并按 totalDiffAmt 倒序。
List<Map<String, Object>> rows = new ArrayList<>();
for (String userId : scopeUserIds) {
TradeAmtBundle own = ownAmt(userId, ownAmtByUserId);
@@ -211,13 +588,39 @@ public class LbDailyUserTradeReportServiceImpl extends ServiceImpl<LbDailyUserTr
row.put("ownDiffAmt", own.diffAmt);
row.put("teamDiffAmt", team.diffAmt);
row.put("totalDiffAmt", total.diffAmt);
// children 用于前端树形展示。
row.put("children", new ArrayList<Map<String, Object>>());
rows.add(row);
}
rows.sort(Comparator.comparing((Map<String, Object> m) -> (BigDecimal) m.get("totalDiffAmt")).reversed());
// 5) 将平铺数据转为树形结构:
// - 仅挂载 scope报表内有交易数据节点避免返回无交易的空节点
// - 若父节点不在 scope则当前节点作为根节点返回。
Map<String, Map<String, Object>> rowByUserId = new HashMap<>();
for (Map<String, Object> row : rows) {
rowByUserId.put((String) row.get("userId"), row);
}
List<Map<String, Object>> treeRows = new ArrayList<>();
for (Map<String, Object> row : rows) {
String userId = (String) row.get("userId");
String parentUserId = parentByUserId.get(userId);
Map<String, Object> parentRow = parentUserId == null ? null : rowByUserId.get(parentUserId);
if (parentRow == null) {
treeRows.add(row);
continue;
}
@SuppressWarnings("unchecked")
List<Map<String, Object>> children = (List<Map<String, Object>>) 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<LbDailyUserTr
}
/**
* 递归下级不含本人在日期范围内四类金额汇总parent_id 为父用户 user_id
* 递归按 totalDiffAmt 倒序排序树节点,确保每层展示稳定
*/
private static void sortTreeByTotalDiffDesc(List<Map<String, Object>> nodes) {
nodes.sort(Comparator.comparing((Map<String, Object> m) -> (BigDecimal) m.get("totalDiffAmt")).reversed());
for (Map<String, Object> node : nodes) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> children = (List<Map<String, Object>>) 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<LbDailyUserTr
Map<String, TradeAmtBundle> ownAmtByUserId,
Map<String, TradeAmtBundle> memo,
Set<String> 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<LbDailyUserTr
}
}
/** 日期范围内按用户汇总的四类金额(与报表字段对应)。 */
/**
* 日期范围内按用户汇总的四类金额(与报表字段对应)。
* 该对象既可表示 own也可表示 team 或 total。
*/
private static final class TradeAmtBundle {
BigDecimal yestodayBuyAmt = BigDecimal.ZERO;
BigDecimal dailySellAmt = BigDecimal.ZERO;

View File

@@ -260,6 +260,7 @@ public class LbDepartmentUserServiceImpl extends ServiceImpl<LbDepartmentUserMap
new LambdaQueryWrapper<LbDailyUserTrade>()
.ge(LbDailyUserTrade::getReportDate, startDate)
.le(LbDailyUserTrade::getReportDate, endDate)
.ge(LbDailyUserTrade::getDailyBuyAmt, 1)
.isNotNull(LbDailyUserTrade::getNickname)
.orderByDesc(LbDailyUserTrade::getUpdatedAt)
.orderByDesc(LbDailyUserTrade::getCreatedAt)