package com.qingjian.module.person.service; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ReUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.qingjian.common.cache.CaseDataCache; import com.qingjian.common.exception.ServerException; import com.qingjian.common.model.person.dto.*; import com.qingjian.common.model.person.entity.Intimacy; import com.qingjian.common.model.person.entity.PersonBasicInfo; import com.qingjian.common.model.person.entity.PersonGroup; import com.qingjian.common.model.person.entity.PersonGroupRel; import com.qingjian.common.model.track.dto.TravelDataDTO; import com.qingjian.common.model.track.dto.TravelPairDTO; import com.qingjian.common.model.track.dto.TravelTimelineDTO; import com.qingjian.common.model.trans.entity.AcctOpenInfo; import com.qingjian.common.model.trans.entity.TransRecord; import com.qingjian.module.govern.serivce.GovernConfService; import com.qingjian.module.person.mapper.IntimacyMapper; import com.qingjian.module.person.mapper.PersonBasicInfoMapper; import com.qingjian.module.person.mapper.PersonGroupMapper; import com.qingjian.module.person.mapper.PersonGroupRelMapper; import com.qingjian.module.plat.service.CacheManageService; import com.qingjian.module.track.mapper.TrackTogetherTravelMapper; import com.qingjian.module.trans.mapper.AcctOpenInfoMapper; import com.qingjian.module.trans.mapper.TransRecordMapper; import lombok.extern.slf4j.Slf4j; import org.noear.solon.annotation.Inject; import org.noear.solon.annotation.Managed; import java.math.BigDecimal; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.IntStream; @Slf4j @Managed public class IntimacyService extends ServiceImpl { @Inject private IntimacyMapper intimacyMapper; @Inject private TransRecordMapper transRecordMapper; @Inject private PersonGroupMapper personGroupMapper; @Inject private PersonGroupRelMapper personGroupRelMapper; @Inject private AcctOpenInfoMapper acctOpenInfoMapper; @Inject private GovernConfService governConfService; @Inject private PersonBasicInfoMapper personBasicInfoMapper; @Inject private CacheManageService cacheManageService; @Inject private TrackTogetherTravelMapper trackTogetherTravelMapper; public List dataList() { return this.list(); } public void update(List intimacyList) { if (CollUtil.isEmpty(intimacyList)) { return; } Integer totalWeight = intimacyList.stream().map(Intimacy::getWeight).reduce(0, Integer::sum); if (totalWeight > 100) { throw new ServerException("权重之和不能大于 100!"); } intimacyMapper.updateById(intimacyList); cacheManageService.removeByPrefix("dp_intimacySummary_"); } // ==================== 亲密度汇总 ==================== public List intimacySummary(String personName) { List allBehaviors = new ArrayList<>(); allBehaviors.addAll(bigTrans(personName)); allBehaviors.addAll(specialDayTrans(personName)); allBehaviors.addAll(samePhoneBinding(personName)); allBehaviors.addAll(keepCall(personName)); allBehaviors.addAll(nightCall(personName)); allBehaviors.addAll(specialDayCall(personName)); allBehaviors.addAll(keepTrans(personName)); allBehaviors.addAll(travelTogether(personName)); Map> groupedByOther = allBehaviors.stream() .filter(i -> StrUtil.isNotEmpty(i.getOtherName())) .collect(Collectors.groupingBy(IntimacyDTO::getOtherName)); List results = new ArrayList<>(); groupedByOther.forEach((otherName, behaviors) -> { if (CollUtil.isEmpty(behaviors)) { return; } int totalIntimacy = behaviors.stream().map(IntimacyDTO::getIntimacy).reduce(0, Integer::sum); List behaviorLabels = behaviors.stream().map(IntimacyDTO::getIntimacyBehavior).distinct().collect(Collectors.toList()); JSONObject detail = mergeDetail(behaviors); IntimacyDTO result = new IntimacyDTO(); result.setOtherName(otherName); result.setIntimacy(totalIntimacy / 100); result.setIntimacyBehavior(StrUtil.join(",", behaviorLabels)); result.setDetail(detail); results.add(result); }); return results.stream() .filter(i -> i.getIntimacy() != 0) .sorted(Comparator.comparingInt(IntimacyDTO::getIntimacy).reversed()) .toList(); } private JSONObject mergeDetail(List behaviors) { return behaviors.stream() .map(IntimacyDTO::getDetail) .flatMap(d -> d.entrySet().stream()) .collect( JSONObject::new, (jsonObj, entry) -> jsonObj.set( entry.getKey(), jsonObj.getInt(entry.getKey(), 0) + (Integer) entry.getValue() ), JSONObject::putAll ); } // ==================== 大额转账 ==================== public List bigTrans(String personName) { Intimacy config = getAndCheckIntimacy("大额转账"); if (config == null) { return List.of(); } int weight = config.getWeight(); JSONObject scoreConfig = parseScores(config); Set personCards = CaseDataCache.getPerCards(personName); if (CollUtil.isEmpty(personCards)) { return List.of(); } List records = transRecordMapper.selectListIntimacy(personCards, governConfService.getTransTableName()); Map> groupedByOther = records.stream() .collect(Collectors.groupingBy(record -> resolveOtherName(record.getOtherName(), record.getOtherCardNo()))); List results = new ArrayList<>(); groupedByOther.forEach((otherName, transRecords) -> { JSONObject detail = new JSONObject(); int above50kCount = 0; int between10kAnd50kCount = 0; for (TransRecord record : transRecords) { if (record.getTransAmount().compareTo(new BigDecimal(50000)) >= 0) { above50kCount++; } else if (record.getTransAmount().compareTo(new BigDecimal(10000)) >= 0 && record.getTransAmount().compareTo(new BigDecimal(50000)) < 0) { between10kAnd50kCount++; } } int score; if (above50kCount > 0) { score = weight * scoreConfig.getInt("大于等于5万"); detail.set("大额交易次数", above50kCount + between10kAnd50kCount); } else if (between10kAnd50kCount > 0) { score = weight * scoreConfig.getInt("1万到5万"); detail.set("大额交易次数", between10kAnd50kCount); } else { return; } results.add(buildIntimacyDTO(otherName, score, "大额转账", weight, detail)); }); return results; } // ==================== 特殊日期特殊转账 ==================== public List specialDayTrans(String personName) { Intimacy config = getAndCheckIntimacy("特殊日期特殊转账"); if (config == null) { return List.of(); } int weight = config.getWeight(); JSONObject scoreConfig = parseScores(config); Set personCards = CaseDataCache.getPerCards(personName); if (CollUtil.isEmpty(personCards)) { return List.of(); } List records = intimacyMapper.specialDm(personName, personCards, governConfService.getTransTableName()); Map> countGroups = records.stream().collect(Collectors.groupingBy(SpecialDmDTO::getCount)); List> topGroups = countGroups.entrySet().stream() .sorted(Map.Entry.>comparingByKey().reversed()) .map(Map.Entry::getValue) .limit(20) .toList(); return IntStream.range(0, topGroups.size()) .mapToObj(i -> { int scoreTier = i < 5 ? 0 : i < 10 ? 1 : 2; int baseScore = scoreTier == 0 ? scoreConfig.getInt("交易次数top1~5") : scoreTier == 1 ? scoreConfig.getInt("交易次数top6~10") : scoreConfig.getInt("交易次数top11~top20"); return topGroups.get(i).stream().map(record -> buildIntimacyDTO( record.getOtherName(), weight * baseScore, "特殊日期特殊转账", weight, buildDetail("特殊日期特殊转账", record.getCount()) )); }) .flatMap(Function.identity()) .collect(Collectors.toList()); } // ==================== 异卡同号 ==================== public List samePhoneBinding(String personName) { Intimacy config = getAndCheckIntimacy("异卡同号"); if (config == null) { return List.of(); } int weight = config.getWeight(); JSONObject scoreConfig = parseScores(config); PersonGroupRel groupRel = personGroupRelMapper.selectOne(Wrappers.lambdaQuery(PersonGroupRel.class) .eq(PersonGroupRel::getPersonName, personName)); if (groupRel == null) { return List.of(); } PersonGroup group = personGroupMapper.selectWithPersonsById(groupRel.getGroupId()); String mainPersonName = null; List personList = group.getPersonList(); Map cardToPerson = new HashMap<>(); for (PersonGroupRel rel : personList) { if (StrUtil.equals("主体", rel.getMainDesc())) { mainPersonName = rel.getPersonName(); } else { Set cards = CaseDataCache.getPerCards(rel.getPersonName()); for (String card : cards) { cardToPerson.put(card, rel.getPersonName()); } } } if (!StrUtil.equals(personName, mainPersonName)) { return List.of(); } Set mainPersonPhones = CaseDataCache.getPerPhones(mainPersonName); List otherAccounts = acctOpenInfoMapper.selectList(new LambdaQueryWrapper() .ne(AcctOpenInfo::getPersonName, mainPersonName) .isNotNull(AcctOpenInfo::getPersonName) .isNotNull(AcctOpenInfo::getPhone)); List matchedAccounts = otherAccounts.stream() .filter(account -> CollUtil.contains(mainPersonPhones, account.getPhone())) .filter(account -> cardToPerson.containsKey(account.getPersonCardNo())) .collect(Collectors.toList()); List results = new ArrayList<>(); if (CollUtil.isNotEmpty(matchedAccounts)) { Map> groupedByName = matchedAccounts.stream() .filter(account -> !StrUtil.isBlank(account.getPersonName())) .collect(Collectors.groupingBy(AcctOpenInfo::getPersonName)); groupedByName.forEach((ownerName, accounts) -> { IntimacyDTO result = new IntimacyDTO(); result.setOtherName(ownerName); result.setIntimacyBehavior("异卡同号"); List matchedCards = accounts.stream().map(AcctOpenInfo::getPersonCardNo).distinct().collect(Collectors.toList()); if (CollUtil.size(matchedCards) >= 2) { result.setIntimacy(weight * scoreConfig.getInt("绑定号码关联银行卡数量大于等于2张")); } else if (CollUtil.size(matchedCards) == 1) { result.setIntimacy(weight * scoreConfig.getInt("绑定号码关联银行卡数量等于1张")); } else { return; } JSONObject detail = new JSONObject(); detail.set("异卡同号", CollUtil.size(matchedCards)); result.setDetail(detail); results.add(result); }); } return results; } // ==================== 频繁联系 ==================== public List keepCall(String personName) { Intimacy config = intimacyMapper.selectOne(Wrappers.lambdaQuery() .eq(Intimacy::getTags, "频繁联系").last("limit 1")); if (config == null) { return List.of(); } JSONObject scoreConfig = parseScores(config); List results = new ArrayList<>(); List callRecords = fetchCallList(personName); Map> countGroups = callRecords.stream() .collect(Collectors.groupingBy(CallListDTO::getCount)); AtomicInteger index = new AtomicInteger(0); countGroups.forEach((count, records) -> { if (index.incrementAndGet() < 6) { records.forEach(record -> { int score = scoreConfig.getInt("联系次数top前5"); results.add(buildIntimacyDTO(record.getOtherName(), config.getWeight() * score, "联系次数", config.getWeight(), buildDetail("联系次数", record.getCount()))); }); } else if (index.incrementAndGet() < 11) { records.forEach(record -> { int score = scoreConfig.getInt("联系次数top6~10"); results.add(buildIntimacyDTO(record.getOtherName(), config.getWeight() * score, "联系次数", config.getWeight(), buildDetail("联系次数", record.getCount()))); }); } else if (index.incrementAndGet() < 21) { records.forEach(record -> { int score = scoreConfig.getInt("联系次数top11~20"); results.add(buildIntimacyDTO(record.getOtherName(), config.getWeight() * score, "联系次数", config.getWeight(), buildDetail("联系次数", record.getCount()))); }); } }); filterEmptyDetail(results); return results; } private List fetchCallList(String personName) { String callTableName = governConfService.getCallTableName(); List records = intimacyMapper.selectCallAllListByName(personName, callTableName); return records.stream() .filter(a -> !ReUtil.isMatch("^(12|95|96|106)", a.getOtherPhone())) .collect(Collectors.toList()); } // ==================== 特殊日期联系 ==================== public List specialDayCall(String personName) { Intimacy config = getAndCheckIntimacy("特殊日期联系"); if (config == null) { return List.of(); } JSONObject scoreConfig = parseScores(config); Set personPhones = CaseDataCache.getPerPhones(personName); if (CollUtil.isEmpty(personPhones)) { return List.of(); } String callTableName = governConfService.getCallTableName(); List records = intimacyMapper.specialCallList(personPhones, callTableName); AtomicInteger index = new AtomicInteger(0); List results = records.stream() .sorted(Comparator.comparing(CallVoiceDTO::getVcCount, Comparator.reverseOrder())) .limit(20).map(record -> { int i = index.incrementAndGet(); int weightedScore = calcSpecialDayScore(i, scoreConfig, config.getWeight()); return buildIntimacyDTO(record.getOtherName(), weightedScore, "特殊日期通话", config.getWeight(), buildDetail("特殊日期通话次数", record.getVcCount())); }).collect(Collectors.toList()); filterEmptyDetail(results); return results; } private int calcSpecialDayScore(int rank, JSONObject scoreConfig, int weight) { int score; if (rank < 6) { score = scoreConfig.getInt("联系次数top1~5", 0); } else if (rank < 11) { score = scoreConfig.getInt("联系次数top6~10", 0); } else if (rank < 21) { score = scoreConfig.getInt("联系次数top11~20", 0); } else { score = scoreConfig.getInt("联系次数在top20以后", 0); } return weight * score; } // ==================== 频繁交易 ==================== public List keepTrans(String personName) { Set personCards = CaseDataCache.getPerCards(personName); if (CollUtil.isEmpty(personCards)) { return List.of(); } List summaries = intimacyMapper.dealCountSummary(personCards, governConfService.getTransTableName()); if (CollUtil.isEmpty(summaries)) { return List.of(); } Integer weight = null; Integer score1to5 = null; Integer score6to10 = null; Integer score11to20 = null; Integer score21plus = null; try { Intimacy config = intimacyMapper.selectOne(Wrappers.lambdaQuery() .eq(Intimacy::getTags, "频繁交易").last("limit 1")); weight = config.getWeight(); JSONObject scoreConfig = parseScores(config); score1to5 = scoreConfig.getInt("交易次数top1~5"); score6to10 = scoreConfig.getInt("交易次数top6~10"); score11to20 = scoreConfig.getInt("交易次数top11~top20"); score21plus = scoreConfig.getInt("交易次数大于top20"); } catch (Exception e) { log.error("亲密度 - 频繁交易配置信息获取", e); } int finalWeight = weight == null ? 15 : weight; int finalScore1to5 = score1to5 == null ? 100 : score1to5; int finalScore6to10 = score6to10 == null ? 60 : score6to10; int finalScore11to20 = score11to20 == null ? 40 : score11to20; int finalScore21plus = score21plus == null ? 0 : score21plus; Map> countGroups = summaries.stream() .collect(Collectors.groupingBy(DealSummaryVO::getDealCount, LinkedHashMap::new, Collectors.toList())); List> groupedSummaries = new ArrayList<>(countGroups.values()); List results = new ArrayList<>(); for (int i = 0; i < groupedSummaries.size(); i++) { List sameCountSummaries = groupedSummaries.get(i); for (DealSummaryVO summary : sameCountSummaries) { IntimacyDTO result = new IntimacyDTO(); String name = summary.getOtherName(); if (StrUtil.isNotEmpty(name)) { result.setOtherName(name); int score = i < 5 ? finalScore1to5 : i < 10 ? finalScore6to10 : i < 20 ? finalScore11to20 : finalScore21plus; result.setIntimacy(score * finalWeight); result.setIntimacyBehavior(buildBehaviorLabel("频繁交易", finalWeight)); result.setDetail(buildDetail("交易次数", summary.getDealCount())); results.add(result); } } } return results.stream().filter(e -> e.getIntimacy() > 0).collect(Collectors.toList()); } // ==================== 同交通工具出行 ==================== public List travelTogether(String personName) { List serviceRecords = new ArrayList<>(); Map> tripMatchMap = new HashMap<>(); PersonBasicInfo personBasicInfo = personBasicInfoMapper.selectOne(Wrappers.lambdaQuery() .eq(PersonBasicInfo::getPersonName, personName), false); if (personBasicInfo != null && StrUtil.isNotEmpty(personBasicInfo.getPersonCertNo())) { List flightTrainRecords = intimacyMapper.travelsTrainFlight(personName, personBasicInfo.getPersonCertNo()); if (CollUtil.isNotEmpty(flightTrainRecords)) { List mappedRecords = flightTrainRecords.stream().map(record -> { TravelDataDTO dto = new TravelDataDTO(); dto.setId(record.getId()); dto.setPersonName(personName); dto.setCompanionName(record.getCompanionName()); dto.setCount(1); return dto; }).toList(); serviceRecords.addAll(mappedRecords); } } List allPersons = personBasicInfoMapper.selectList(Wrappers.emptyWrapper()); List travelRecords = trackTogetherTravelMapper.travels(null, null, personName); Map> timeTripGroup = travelRecords.stream().collect(Collectors.groupingBy(item -> String.format("%s_%s", StrUtil.isBlank(item.getTripDateTime()) ? "" : item.getTripDateTime().trim(), StrUtil.isBlank(item.getTripNo()) ? "" : item.getTripNo().trim()))); for (PersonBasicInfo otherPerson : allPersons) { if (Objects.equals(otherPerson.getPersonName(), personName)) { continue; } List otherTravelRecords = trackTogetherTravelMapper.travels(null, null, otherPerson.getPersonName()); if (CollUtil.isEmpty(otherTravelRecords)) { continue; } otherTravelRecords.forEach(record -> { List matchedRecords = timeTripGroup.get(String.format("%s_%s", StrUtil.isBlank(record.getTripDateTime()) ? "" : record.getTripDateTime().trim(), StrUtil.isBlank(record.getTripNo()) ? "" : record.getTripNo().trim())); if (CollUtil.isEmpty(matchedRecords)) { return; } AtomicInteger matchCount = new AtomicInteger(); TravelDataDTO dto = new TravelDataDTO(); matchCount.set(matchedRecords.size()); dto.setId(record.getId()); dto.setPersonName(personName); dto.setCompanionName(otherPerson.getPersonName()); dto.setCount(matchCount.get()); List matchedGroup = new ArrayList<>(matchedRecords); matchedGroup.add(record); tripMatchMap.put(String.format("%s_%s", StrUtil.isBlank(record.getTripDateTime()) ? "" : record.getTripDateTime().trim(), StrUtil.isBlank(record.getTripNo()) ? "" : record.getTripNo().trim()), matchedGroup); }); } List timelines = buildTravelTimeline(tripMatchMap, serviceRecords); Integer weight = null; Integer score10Plus = null; Integer score5to9 = null; Integer score1to4 = null; try { Intimacy config = intimacyMapper.selectOne(Wrappers.lambdaQuery() .eq(Intimacy::getTags, "同交通工具出行").last("limit 1")); weight = config.getWeight(); JSONObject scoreConfig = parseScores(config); score10Plus = scoreConfig.getInt("同行次数大于等于10次"); score5to9 = scoreConfig.getInt("同行次数5~9次"); score1to4 = scoreConfig.getInt("同行次数1~4次"); } catch (Exception e) { log.error("亲密度 - 同交通工具出行配置信息获取", e); } int finalScore10Plus = score10Plus == null ? 100 : score10Plus; int finalScore5to9 = score5to9 == null ? 80 : score5to9; int finalScore1to4 = score1to4 == null ? 70 : score1to4; int finalWeight = weight == null ? 5 : weight; List results = timelines.stream().map(timeline -> { IntimacyDTO result = new IntimacyDTO(); if (timeline.getPersonName().equals(personName)) { result.setOtherName(timeline.getCompanionName()); } else { result.setOtherName(timeline.getPersonName()); } int score = timeline.getCount() >= 10 ? finalScore10Plus : timeline.getCount() >= 5 ? finalScore5to9 : timeline.getCount() >= 1 ? finalScore1to4 : 0; result.setIntimacy(score * finalWeight); result.setIntimacyBehavior(buildBehaviorLabel("同交通工具出行", finalWeight)); result.setDetail(buildDetail("同行次数", timeline.getCount())); return result; }).toList(); return results.stream().filter(e -> e.getIntimacy() > 0).collect(Collectors.toList()); } private List buildTravelTimeline(Map> tripMatchMap, List serviceRecords) { List results = new ArrayList<>(); tripMatchMap.forEach((key, matchedGroup) -> { if (CollUtil.isEmpty(matchedGroup) || matchedGroup.size() < 2) { return; } TravelTimelineDTO timelineDTO = new TravelTimelineDTO(); TravelDataDTO first = matchedGroup.get(0); TravelDataDTO second = matchedGroup.get(1); timelineDTO.setPersonName(first.getPersonName()); timelineDTO.setPersonName(second.getPersonName()); timelineDTO.setCount(1); timelineDTO.setIdList(matchedGroup.stream().map(TravelDataDTO::getId).collect(Collectors.toList())); results.add(timelineDTO); }); Map> pairGroup = serviceRecords.stream() .collect(Collectors.groupingBy(item -> TravelPairDTO.create(item.getPersonName(), item.getCompanionName()))); pairGroup.forEach((pair, pairRecords) -> { if (CollUtil.isEmpty(pairRecords)) { return; } TravelTimelineDTO timelineDTO = new TravelTimelineDTO(); timelineDTO.setPersonName(pair.getPersonName()); timelineDTO.setCompanionName(pair.getCompanionName()); timelineDTO.setCount(pairRecords.size()); timelineDTO.setIdList(pairRecords.stream().map(TravelDataDTO::getId).collect(Collectors.toList())); results.add(timelineDTO); }); return aggregateTravelTimeline(results); } private List aggregateTravelTimeline(List timelines) { List results = new ArrayList<>(); Map> pairGroup = timelines.stream() .collect(Collectors.groupingBy(t -> t.getPersonName() + "-" + t.getCompanionName())); pairGroup.forEach((key, items) -> { List split = StrUtil.split(key, "-"); TravelTimelineDTO merged = new TravelTimelineDTO(); merged.setPersonName(split.get(0)); merged.setCompanionName(split.get(1)); Integer totalCount = items.stream().map(TravelTimelineDTO::getCount).reduce(0, Integer::sum); List ids = items.stream().flatMap(t -> t.getIdList().stream()).collect(Collectors.toList()); merged.setCount(totalCount); merged.setIdList(ids); results.add(merged); }); return results; } // ==================== 夜间通话 ==================== public List nightCall(String personName) { Intimacy config = getAndCheckIntimacy("夜间通话"); if (config == null) { return List.of(); } JSONObject scoreConfig = parseScores(config); List nightCallRecords = new ArrayList<>(); buildNightCallRecords(personName, nightCallRecords); if (CollUtil.isEmpty(nightCallRecords)) { return List.of(); } List results = new ArrayList<>(); NightVoiceDTO nightCall = nightCallRecords.getFirst(); List topRecords = nightCall.getTop5List(); Map> countGroups = topRecords.stream() .collect(Collectors.groupingBy(NightVoiceTop5DTO::getCmnCount)); AtomicInteger index = new AtomicInteger(0); countGroups.forEach((count, records) -> { if (index.incrementAndGet() < 6) { records.forEach(record -> { int score = scoreConfig.getInt("夜间通话次数top前5"); results.add(buildIntimacyDTO(record.getOtherPhone(), config.getWeight() * score, "夜间通话次数", config.getWeight(), buildDetail("夜间通话次数", record.getCmnCount()))); }); } else if (index.incrementAndGet() < 11) { records.forEach(record -> { int score = scoreConfig.getInt("夜间通话次数top6~10"); results.add(buildIntimacyDTO(record.getOtherPhone(), config.getWeight() * score, "夜间通话次数", config.getWeight(), buildDetail("夜间通话次数", record.getCmnCount()))); }); } else if (index.incrementAndGet() < 21) { records.forEach(record -> { int score = scoreConfig.getInt("夜间通话次数top11~20"); results.add(buildIntimacyDTO(record.getOtherPhone(), config.getWeight() * score, "夜间通话次数", config.getWeight(), buildDetail("夜间通话次数", record.getCmnCount()))); }); } }); filterEmptyDetail(results); return results; } // ==================== 工具方法 ==================== private Intimacy getAndCheckIntimacy(String tags) { return intimacyMapper.selectOne(new LambdaQueryWrapper() .eq(Intimacy::getTags, tags)); } private JSONObject parseScores(Intimacy intimacy) { return JSONUtil.parseObj(intimacy.getScores()); } private String resolveOtherName(String otherName, String fallback) { return StrUtil.emptyToDefault(otherName, fallback); } private String buildBehaviorLabel(String behavior, int weight) { return behavior + "-" + "(权重" + weight + "%)"; } private JSONObject buildDetail(String key, Object value) { JSONObject detail = new JSONObject(); detail.set(key, value); return detail; } private IntimacyDTO buildIntimacyDTO(String otherName, int intimacy, String behavior, int weight, JSONObject detail) { IntimacyDTO dto = new IntimacyDTO(); dto.setOtherName(otherName); dto.setIntimacy(intimacy); dto.setIntimacyBehavior(buildBehaviorLabel(behavior, weight)); dto.setDetail(detail); return dto; } private void filterEmptyDetail(List dtos) { for (IntimacyDTO dto : dtos) { JSONObject entries = new JSONObject(); dto.getDetail().forEach((k, v) -> { if (!v.equals(0)) { entries.set(k, v); } }); dto.setDetail(entries); } dtos.removeIf(a -> a.getDetail().isEmpty()); } private void buildNightCallRecords(String personName, List nightCallList) { String callTableName = governConfService.getCallTableName(); List rawRecords = intimacyMapper.selectAllDataByName(personName, callTableName); Map> groupedByName = rawRecords.stream() .filter(a -> !ReUtil.isMatch("^(12|95|96|106)", a.getOtherName())) .collect(Collectors.groupingBy(NightVoiceTempDTO::getOtherName)); for (Map.Entry> nameEntry : groupedByName.entrySet()) { NightVoiceDTO nightCall = new NightVoiceDTO(); List topRecords = new ArrayList<>(); List nameRecords = nameEntry.getValue(); Map> groupedByPhone = nameRecords.stream() .collect(Collectors.groupingBy(NightVoiceTempDTO::getOtherName)); for (Map.Entry> phoneEntry : groupedByPhone.entrySet()) { NightVoiceTop5DTO topRecord = new NightVoiceTop5DTO(); String phone = phoneEntry.getKey(); List phoneRecords = phoneEntry.getValue(); List yearMonths = phoneRecords.stream().map(record -> { LocalDate callDate = record.getCallDate(); return callDate.format(DateTimeFormatter.ofPattern("yyyy-MM")); }).distinct().sorted().toList(); topRecord.setOtherPhone(phone); topRecord.setCmnCount(phoneRecords.size()); topRecord.setTotalMonth(yearMonths.size()); topRecords.add(topRecord); } topRecords = topRecords.stream() .filter(record -> record.getTotalMonth() >= (Long) 2L) .sorted(Comparator.comparing(NightVoiceTop5DTO::getCmnCount, Comparator.reverseOrder())) .limit(20) .collect(Collectors.toList()); nightCall.setTop5List(topRecords); nightCallList.add(nightCall); } } }