806 lines
40 KiB
Java
806 lines
40 KiB
Java
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 A:parent_id 本身就是父 userId
|
||
if (allUserIds.contains(parentRef)) {
|
||
parentUserId = parentRef;
|
||
resolvedBy = "parent_user_id";
|
||
// case B:parent_id 是父记录id(UUID),反查映射到父 userId
|
||
} else if (userIdByRecordId.containsKey(parentRef)) {
|
||
parentUserId = userIdByRecordId.get(parentRef);
|
||
resolvedBy = "parent_record_id";
|
||
}
|
||
// 两种都解析失败:说明关系数据不完整/不一致,记录日志便于排查。
|
||
if (parentUserId == null || parentUserId.isEmpty()) {
|
||
log.warn("dept relation parent unresolved, tenantId={}, userId={}, name={}, parentId={}",
|
||
tenantIdTrim, uid, row.getName(), parentRef);
|
||
continue;
|
||
}
|
||
List<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;
|
||
}
|
||
}
|
||
}
|