Files
smartDriveEE/src/main/java/com/rj/service/impl/LbDailyUserTradeReportServiceImpl.java

806 lines
40 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/**
* <p>
* 当天用户交易报表 服务实现类
* </p>
*/
@Service
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) {
Map<String, Object> result = new HashMap<>();
LocalDateTime start = reportDate.atStartOfDay();
LocalDateTime end = reportDate.plusDays(1).atStartOfDay();
LambdaQueryWrapper<LbDailyUserTradeReport> 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<LbDailyUserTradeReport> 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<String, Object> 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<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)
.orderByDesc(LbDailyUserTradeReport::getUpdatedAt)
.orderByDesc(LbDailyUserTradeReport::getCreatedAt)
.last("limit 1");
List<LbDailyUserTradeReport> 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<LbDailyUserTradeReport> 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<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 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<String, Object> listUserTradeAmountWithTeamByDateRangeAndTenant(
LocalDate startDate, LocalDate endDate, String tenantId) {
Map<String, Object> 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<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);
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<String> scopeUserIds = new HashSet<>(ownAmtByUserId.keySet());
// 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;
}
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 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()) {
list.sort(String::compareTo);
}
}
// 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);
TradeAmtBundle team = teamAmt(userId, teamAmtMemo);
TradeAmtBundle total = own.addCopy(team);
Map<String, Object> 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<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", "查询成功");
// 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<String, TradeAmtBundle> ownAmtByUserId) {
TradeAmtBundle b = ownAmtByUserId.get(userId);
return b != null ? b : TradeAmtBundle.zeros();
}
private static TradeAmtBundle teamAmt(String userId, Map<String, TradeAmtBundle> teamAmtMemo) {
TradeAmtBundle b = teamAmtMemo.get(userId);
return b != null ? b : TradeAmtBundle.zeros();
}
/**
* 递归按 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,
Map<String, List<String>> childrenByParentUserId,
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();
}
try {
TradeAmtBundle sum = TradeAmtBundle.zeros();
List<String> 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;
}
}
}