@@ -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 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 ( ) ) {
@@ -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 ;