logging rest request; logging query time for every mapper; optimization query and refactoring some methods

This commit is contained in:
etreschenkov 2025-09-22 22:02:55 +03:00
parent fdbec299d6
commit bddc5bb1fa
14 changed files with 482 additions and 192 deletions

139
pom.xml
View file

@ -27,6 +27,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
@ -122,8 +126,83 @@
<version>3.0.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<profiles>
<profile>
<id>docker-push-dev</id>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.45.0</version>
<executions>
<execution>
<id>Build and deploy docker container</id>
<phase>install</phase>
<goals>
<!--<goal>build</goal>-->
<goal>push</goal>
</goals>
</execution>
</executions>
<configuration>
<images>
<image>
<name>${project.artifactId}:${project.version}</name>
<registry>${registry.url}</registry>
<build>
<dockerFile>${project.basedir}/Dockerfile</dockerFile>
<assembly>
<inline>
<files>
<file>
<source>${project.build.directory}/${project.build.finalName}.jar</source>
<outputDirectory>.</outputDirectory>
</file>
<file>
<source>${project.basedir}/apm-agent/elastic-apm-agent-1.45.0.jar</source>
<outputDirectory>.</outputDirectory>
</file>
<file>
<source>${project.basedir}/start-credit-tracker.sh</source>
<outputDirectory>.</outputDirectory>
</file>
<file>
<source>${project.basedir}/credit-tracker.properties</source>
<outputDirectory>.</outputDirectory>
</file>
<file>
<source>${project.basedir}/monitoring-request.xsd</source>
<outputDirectory>.</outputDirectory>
</file>
<file>
<source>${project.basedir}/signal-package.xsd</source>
<outputDirectory>.</outputDirectory>
</file>
</files>
</inline>
</assembly>
<tags>
<tag>${project.version}</tag>
</tags>
</build>
</image>
</images>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</profile>
</profiles>
<build>
<finalName>credit-tracker</finalName>
<plugins>
@ -181,65 +260,7 @@
</includeOnlyProperties>
</configuration>
</plugin>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.45.0</version>
<executions>
<execution>
<id>Build and deploy docker container</id>
<phase>install</phase>
<goals>
<goal>build</goal>
<goal>push</goal>
</goals>
</execution>
</executions>
<configuration>
<images>
<image>
<name>${project.artifactId}:${project.version}</name>
<registry>${registry.url}</registry>
<build>
<dockerFile>${project.basedir}/Dockerfile</dockerFile>
<assembly>
<inline>
<files>
<file>
<source>${project.build.directory}/${project.build.finalName}.jar</source>
<outputDirectory>.</outputDirectory>
</file>
<file>
<source>${project.basedir}/apm-agent/elastic-apm-agent-1.45.0.jar</source>
<outputDirectory>.</outputDirectory>
</file>
<file>
<source>${project.basedir}/start-credit-tracker.sh</source>
<outputDirectory>.</outputDirectory>
</file>
<file>
<source>${project.basedir}/credit-tracker.properties</source>
<outputDirectory>.</outputDirectory>
</file>
<file>
<source>${project.basedir}/monitoring-request.xsd</source>
<outputDirectory>.</outputDirectory>
</file>
<file>
<source>${project.basedir}/signal-package.xsd</source>
<outputDirectory>.</outputDirectory>
</file>
</files>
</inline>
</assembly>
<tags>
<tag>${project.version}</tag>
</tags>
</build>
</image>
</images>
</configuration>
</plugin>
</plugins>
</build>

View file

@ -0,0 +1,28 @@
package ru.nbch.credit_tracker.component;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Slf4j
@Component
public class MapperTimingAspect {
@Around("within(@org.apache.ibatis.annotations.Mapper *)")
public Object timeMapper(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try {
return pjp.proceed();
} finally {
long tookMs = (System.nanoTime() - start) / 1_000_000;
String method = pjp.getSignature().toShortString();
if (tookMs > 200) {
log.warn("Slow mapper call {} took {} ms", method, tookMs);
} else {
log.debug("Mapper call {} took {} ms", method, tookMs);
}
}
}
}

View file

@ -0,0 +1,111 @@
package ru.nbch.credit_tracker.config.rest.filters;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
@Component
@Slf4j
@Order(0)
public class HttpLoggingFilter extends OncePerRequestFilter {
private static final Set<String> SENSITIVE_HEADERS = Set.of("authorization", "cookie", "set-cookie");
private static final int MAX_BODY = 10 * 1024;
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getRequestURI();
return path.startsWith("/actuator/health") || path.startsWith("/swagger")
|| path.startsWith("/v3/api-docs") || path.startsWith("/static/");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
long startNs = System.nanoTime();
ContentCachingRequestWrapper req = wrapRequest(request);
ContentCachingResponseWrapper resp = new ContentCachingResponseWrapper(response);
String correlationId = Optional.ofNullable(request.getHeader("X-Correlation-Id"))
.filter(StringUtils::hasText)
.orElse(UUID.randomUUID().toString());
resp.setHeader("X-Correlation-Id", correlationId);
try {
chain.doFilter(req, resp);
} finally {
long tookMs = (System.nanoTime() - startNs) / 1_000_000;
String requestBody = readBody(req.getContentAsByteArray(), req.getCharacterEncoding());
String responseBody = readBody(resp.getContentAsByteArray(), resp.getCharacterEncoding());
Map<String, String> reqHeaders = headersMap(req);
Map<String, String> respHeaders = headersMap(resp);
mask(reqHeaders);
mask(respHeaders);
log.info(
"HTTP {} {}{} | status={} | took={}ms | cid={} | reqHeaders={} | reqBody={} | respHeaders={} | respBody={}",
req.getMethod(),
req.getRequestURI(),
req.getQueryString() == null ? "" : "?" + req.getQueryString(),
resp.getStatus(),
tookMs,
correlationId,
reqHeaders,
requestBody,
respHeaders,
responseBody
);
resp.copyBodyToResponse();
}
}
private static ContentCachingRequestWrapper wrapRequest(HttpServletRequest request) {
return (request instanceof ContentCachingRequestWrapper c) ? c : new ContentCachingRequestWrapper(request, MAX_BODY);
}
private static String readBody(byte[] buf, String encoding) {
if (buf == null || buf.length == 0) return "";
var bytes = buf.length > MAX_BODY ? Arrays.copyOf(buf, MAX_BODY) : buf;
String body = new String(bytes, encoding != null ? java.nio.charset.Charset.forName(encoding) : StandardCharsets.UTF_8);
if (buf.length > MAX_BODY) body += "…[truncated]";
return body;
}
private static Map<String, String> headersMap(HttpServletRequest req) {
return Collections.list(req.getHeaderNames()).stream()
.collect(Collectors.toMap(h -> h, req::getHeader, (a, b) -> a, LinkedHashMap::new));
}
private static Map<String, String> headersMap(HttpServletResponse resp) {
return resp.getHeaderNames().stream()
.collect(Collectors.toMap(h -> h, resp::getHeader, (a, b) -> a, LinkedHashMap::new));
}
private static void mask(Map<String, String> headers) {
headers.replaceAll((k, v) -> SENSITIVE_HEADERS.contains(k.toLowerCase()) ? "***" : v);
}
}

View file

@ -15,6 +15,7 @@ public interface PhoneFidMapMapper {
List<PhoneFidMap> findFullPhoneFidMap(@Param("packageId") Integer packageId, @Param("offset") int offset, @Param("limit") int limit);
void updatePhoneFidMaps(@Param("subjects") List<PhoneFidMap> subjects);
void updatePhoneFid(@Param("subject") PhoneFidMap subject);
void updatePhoneFidMapsErrors(@Param("subjects") List<PhoneFidMap> subjects);

View file

@ -1,8 +1,22 @@
package ru.nbch.credit_tracker.service;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.config.settings.CreditTrackerSettings;
import ru.nbch.credit_tracker.entities.dto.Hit;
@ -22,21 +36,6 @@ import ru.nbch.credit_tracker.service.xml.stax.MonitoringSignalResponseStaxXmlWr
import ru.nbch.credit_tracker.service.xml.stax.StaxXmlProcessor;
import ru.nbch.credit_tracker.utils.CTUtil;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* Сервис периодического опроса официального сервиса сигналов на наличие новых отчетов по пакетам находящимся в статусе ON_MONITORING
*/
@ -60,9 +59,9 @@ public class MonitoringService {
}
public void run() {
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(new CustomizableThreadFactory("monitoring-"));
long period = config.getMonitoringInterval() != null ? config.getMonitoringInterval() : 15;
scheduler.scheduleAtFixedRate(this::monitor, 0, period, TimeUnit.MINUTES);
// ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(new CustomizableThreadFactory("monitoring-"));
// long period = config.getMonitoringInterval() != null ? config.getMonitoringInterval() : 15;
// scheduler.scheduleAtFixedRate(this::monitor, 0, period, TimeUnit.MINUTES);
}
private void monitor() {

View file

@ -15,6 +15,7 @@ import ru.nbch.credit_tracker.entities.external_request.signals.Signals;
import ru.nbch.credit_tracker.entities.external_response.monitoring_report_error.MonitoringReportErrors;
import ru.nbch.credit_tracker.entities.user_request.monitoring_request.MonitoringRequest;
import ru.nbch.credit_tracker.enums.Status;
import ru.nbch.credit_tracker.mapper.signal.PhoneFidMapMapper;
import ru.nbch.credit_tracker.service.indic.FlagsService;
import ru.nbch.credit_tracker.service.indic.PersonService;
import ru.nbch.credit_tracker.service.indic.PhoneService;
@ -43,6 +44,7 @@ public class SignalService {
private final HitsService hitsService;
private final UserPackageService userPackageService;
private final PhoneFidMapService phoneFidMapService;
private final PhoneFidMapMapper phoneFidMapMapper;
private final PhoneService phoneService;
private final FlagsService flagsService;
private final PersonService personService;
@ -54,6 +56,7 @@ public class SignalService {
HitsService hitsService,
UserPackageService userPackageService,
PhoneFidMapService phoneFidMapService,
PhoneFidMapMapper mapper,
PhoneService phoneService,
FlagsService flagsService,
PersonService personService,
@ -62,6 +65,7 @@ public class SignalService {
this.hitsService = hitsService;
this.userPackageService = userPackageService;
this.phoneFidMapService = phoneFidMapService;
this.phoneFidMapMapper = mapper;
this.phoneService = phoneService;
this.flagsService = flagsService;
this.personService = personService;
@ -125,7 +129,7 @@ public class SignalService {
}
if (!filteredItems.isEmpty()) {
boolean fidsFound = updateFlags(filteredItems, packageId);
boolean fidsFound = updateFlags2(filteredItems, packageId);
if (noFidsFound) {
noFidsFound = !fidsFound;
}
@ -187,6 +191,26 @@ public class SignalService {
return fidsFound;
}
public boolean updateFlags2(List<PhoneFidMap> subjects, Integer packageId) {
log.info("Start calculating fids and flags for subjects batch. PackageId: {}", packageId);
Map<Integer, PhoneFidMap> fidsMap = phoneService.defineFidByPhone(subjects);
Map<Integer, FlagsService.FlagsData> flagsDataMap = flagsService.findFlags(fidsMap.keySet().stream().toList());
for (Map.Entry<Integer, FlagsService.FlagsData> entry : flagsDataMap.entrySet()) {
Integer fid = entry.getKey();
FlagsService.FlagsData flagsData = entry.getValue();
PhoneFidMap phoneFidMap = fidsMap.get(fid);
phoneFidMap.setFid(fid);
phoneFidMap.setFlagDebt5000(flagsData.getFlagDebt5000());
phoneFidMap.setFlag90days(flagsData.getFlag90days());
phoneFidMap.setCalculatedAt(LocalDateTime.now());
phoneFidMapMapper.updatePhoneFid(phoneFidMap);
}
log.info("Finish calculating fids and flags for subjects. PackageId: {}. Batch size: {}", packageId, subjects.size());
return !fidsMap.isEmpty();
}
public void rejectPackage(MonitoringRequest request) {
UserPackage userPackage = UserPackage.builder()
.userId(request.getUserId())

View file

@ -1,7 +1,10 @@
package ru.nbch.credit_tracker.service;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.function.Predicate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.config.settings.CreditTrackerSettings;
import ru.nbch.credit_tracker.entities.dto.PhoneFidMap;
@ -9,14 +12,6 @@ import ru.nbch.credit_tracker.entities.dto.UserPackage;
import ru.nbch.credit_tracker.enums.Status;
import ru.nbch.credit_tracker.service.signal.SignalRestService;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
/**
* Сервис периодического обновления фидов и флагов просрочек по пакетам находящимся в статусе ON_MONITORING
* По обновленным пакетам формируется новый запрос постановки на мониторинг в официальный сервис сигналов
@ -43,8 +38,8 @@ public class UpdatingFlagsService {
}
public void run() {
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(new CustomizableThreadFactory("updating-flags-"));
scheduler.scheduleAtFixedRate(this::update, 0, updateFlagsInterval, TimeUnit.DAYS);
// ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(new CustomizableThreadFactory("updating-flags-"));
// scheduler.scheduleAtFixedRate(this::update, 0, updateFlagsInterval, TimeUnit.DAYS);
}
private void update() {

View file

@ -1,20 +1,21 @@
package ru.nbch.credit_tracker.service.indic;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.mapper.indic.FlagsMapper;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.mapper.indic.FlagsMapper;
@Service
public class FlagsService {
private static final Logger log = LoggerFactory.getLogger(FlagsService.class);
private final FlagsMapper mapper;
public FlagsService(FlagsMapper mapper) {
@ -25,11 +26,13 @@ public class FlagsService {
if (fids.isEmpty()) {
return new HashMap<>();
}
Map<Integer, FlagsData> result = new HashMap<>();
Map<Integer, Integer> flags5000 = calcFlag5000(fids);
Map<Integer, Integer> flags90DaysByPastDueArrear = calcFlag90DaysByPastDueArrear(fids);
Map<Integer, Integer> flags90DaysByRuPaymt = calcFlag90DaysByRuPaymt(fids);
List<Integer> distinctFids = fids.stream().distinct().toList();
log.debug("Fids count: {}", distinctFids.size());
Map<Integer, Integer> flags5000 = calcFlag5000(distinctFids);
Map<Integer, Integer> flags90DaysByPastDueArrear = calcFlag90DaysByPastDueArrear(distinctFids);
Map<Integer, Integer> flags90DaysByRuPaymt = calcFlag90DaysByRuPaymt(distinctFids);
Map<Integer, FlagsData> result = new HashMap<>();
fids.forEach(fid -> {
Integer flagDebt5000 = flags5000.get(fid);
if (flagDebt5000 == null) {
@ -55,15 +58,11 @@ public class FlagsService {
* Если ни по одному счёту условие не выполняется или счетов вообще не найдено, флаг = 0.
*/
private Map<Integer, Integer> calcFlag5000(List<Integer> fids) {
return mapper.findFlagsDebt5000(fids).stream()
.collect(Collectors.toMap(FlagFidData::getFid, FlagFidData::getFlag,
(existing, replacement) -> {
if (existing == 1) {
return existing;
} else {
return replacement;
}
}));
log.debug("Start calculating flag 5000");
List<FlagFidData> rows = mapper.findFlagsDebt5000(fids);
Map<Integer, Integer> res = makeFidFlagMapByRs(fids, rows);
log.debug("Complete calculating flag 5000");
return res;
}
/**
@ -72,31 +71,33 @@ public class FlagsService {
* Если ни по одной записи условия не выполняются или записей вообще не найдено, флаг = 0.
*/
private Map<Integer, Integer> calcFlag90DaysByPastDueArrear(List<Integer> fids) {
log.debug("Start calculating flag 90 by PastDueArrear");
LocalDate yearAgo = LocalDate.now().minusYears(1);
return mapper.findFlag90ByPastDueArrear(fids, yearAgo).stream()
.collect(Collectors.toMap(FlagFidData::getFid, FlagFidData::getFlag,
(existing, replacement) -> {
if (existing == 1) {
return existing;
} else {
return replacement;
}
}));
List<FlagFidData> rows = mapper.findFlag90ByPastDueArrear(fids, yearAgo);
Map<Integer, Integer> res = makeFidFlagMapByRs(fids, rows);
log.debug("Complete calculating flag 90 by PastDueArrear");
return res;
}
private Map<Integer, Integer> calcFlag90DaysByRuPaymt(List<Integer> fids) {
log.debug("Start calculating flag 90 by ruPaymt");
LocalDate yearAgo = LocalDate.now().minusYears(1);
return mapper.findFlag90ByRuPaymt(fids, yearAgo).stream()
.collect(Collectors.toMap(FlagFidData::getFid, FlagFidData::getFlag,
(existing, replacement) -> {
if (existing == 1) {
return existing;
} else {
return replacement;
}
}));
List<FlagFidData> rows = mapper.findFlag90ByRuPaymt(fids, yearAgo);
Map<Integer, Integer> res = makeFidFlagMapByRs(fids, rows);
log.debug("Complete calculating flag 90 by ruPaymt");
return res;
}
private Map<Integer, Integer> makeFidFlagMapByRs(List<Integer> fids, List<FlagFidData> rows) {
Map<Integer, Integer> result = new HashMap<>();
for (Integer fid : fids) {
result.put(fid, 0);
}
for (FlagFidData row : rows) {
result.put(row.getFid(), row.getFlag() != null ? row.getFlag() : 0);
}
return result;
}
@Getter
@Setter
@AllArgsConstructor

View file

@ -1,17 +1,19 @@
package ru.nbch.credit_tracker.service.indic;
import lombok.Getter;
import lombok.Setter;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.entities.dto.PhoneFidMap;
import ru.nbch.credit_tracker.mapper.indic.PhoneMapper;
import java.time.LocalDate;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.stereotype.Service;
import ru.nbch.credit_tracker.entities.dto.PhoneFidMap;
import ru.nbch.credit_tracker.mapper.indic.PhoneMapper;
@Service
public class PhoneService {
@ -31,6 +33,37 @@ public class PhoneService {
return convertFidsDataToMap(mapper.findFids(phones));
}
public Map<Integer, PhoneFidMap> defineFidByPhone(List<PhoneFidMap> subjects) {
Map<Integer, PhoneFidMap> phoneFidMap = new HashMap<>();
List<String> phones = subjects.stream().
map(subject -> "9" + subject.getPhone())
.toList();
subjects.sort(
Comparator.comparing(
PhoneFidMap::getPhone,
Comparator.naturalOrder()
)
);
//отсортированный список по NUMBER, FID и FILE_SINCE_DT
List<FidsData> phonesByFid = mapper.findFids(phones);
Iterator<FidsData> iterator = phonesByFid.iterator();
for (PhoneFidMap phoneFid : subjects) {
while (iterator.hasNext()) {
FidsData fidsData = iterator.next();
if (fidsData.getPhone().equals(phoneFid.getPhone())) {
//fixme доступ по ссылке, страшно, придумать что-то еще
phoneFidMap.put(fidsData.getFid(), phoneFid);
iterator.remove();
break;
}
iterator.remove();
}
}
return phoneFidMap;
}
private Map<String, Integer> convertFidsDataToMap(List<FidsData> fidsData) {
if (fidsData == null || fidsData.isEmpty()) {
return new HashMap<>();
@ -50,6 +83,8 @@ public class PhoneService {
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public static class FidsData {
private Integer fid;
private String phone;

View file

@ -8,7 +8,7 @@
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
</appender>

View file

@ -11,70 +11,70 @@
</resultMap>
<select id="findFlagsDebt5000" resultMap="FlagsFidDataResultMap">
SELECT rp.FID as fid,
CASE
WHEN EXISTS (SELECT 1
FROM RU_PASTDUEARREAR rp2
WHERE rp2.FID = rp.FID
AND rp2.ACC_SERIAL_NUM = rp.ACC_SERIAL_NUM
AND rp2.REPORTED_DT = rp.MAX_REP_DATE
AND rp2.AMT_PAST_DUE > 5000) THEN 1
ELSE 0
END as flag
FROM (SELECT FID,
ACC_SERIAL_NUM,
MAX(REPORTED_DT) as MAX_REP_DATE
WITH latest AS (
SELECT
fid,
acc_serial_num,
amt_past_due,
ROW_NUMBER() OVER (PARTITION BY fid, acc_serial_num ORDER BY reported_dt DESC) AS rn
FROM RU_PASTDUEARREAR
WHERE FID IN
WHERE fid IN
<foreach item="fid" collection="fids" open="(" separator="," close=")">
#{fid}
</foreach>
GROUP BY FID, ACC_SERIAL_NUM) rp
GROUP BY rp.FID, rp.ACC_SERIAL_NUM, rp.MAX_REP_DATE
SELECT
fid,
CASE WHEN SUM(CASE WHEN amt_past_due > 5000 THEN 1 ELSE 0 END) > 0
THEN 1 ELSE 0
END AS flag
FROM latest WHERE rn = 1
GROUP BY fid
WITH UR
</select>
<select id="findFlag90ByPastDueArrear" resultMap="FlagsFidDataResultMap">
SELECT rp.FID as fid,
CASE
WHEN EXISTS (SELECT 1
FROM RU_PASTDUEARREAR rp2
WHERE rp2.FID = rp.FID
AND rp2.REPORTED_DT >= #{reportedDate}
AND rp2.DAYS_PAST_DUE > 90) THEN 1
ELSE 0
END as flag
FROM (SELECT FID,
DAYS_PAST_DUE
FROM RU_PASTDUEARREAR
WHERE FID IN
WITH latest AS (
SELECT
r.fid,
r.days_past_due,
ROW_NUMBER() OVER (PARTITION BY r.fid ORDER BY r.reported_dt DESC) AS rn
FROM RU_PASTDUEARREAR r
WHERE r.fid IN
<foreach item="fid" collection="fids" open="(" separator="," close=")">
#{fid}
</foreach>
GROUP BY FID, DAYS_PAST_DUE) rp
GROUP BY rp.FID, rp.DAYS_PAST_DUE
AND r.reported_dt >= #{reportedDate}
)
SELECT
fid,
CASE WHEN MAX(CASE WHEN days_past_due > 90 THEN 1 ELSE 0 END) = 1
THEN 1 ELSE 0
END AS flag
FROM latest WHERE rn = 1
GROUP BY fid
WITH UR
</select>
<select id="findFlag90ByRuPaymt" resultMap="FlagsFidDataResultMap">
SELECT rp.FID as fid,
CASE
WHEN EXISTS (SELECT 1
FROM RU_PAYMT rp2
WHERE rp2.FID = rp.FID
AND rp2.REPORTED_DT >= #{reportedDate}
AND rp2.DAYS_PAST_DUE > 90) THEN 1
ELSE 0
END as flag
FROM (SELECT FID,
DAYS_PAST_DUE
FROM RU_PAYMT
WHERE FID IN
WITH latest AS (
SELECT
r.fid,
r.days_past_due,
ROW_NUMBER() OVER (PARTITION BY r.fid ORDER BY r.reported_dt DESC) AS rn
FROM RU_PAYMT r
WHERE r.fid IN
<foreach item="fid" collection="fids" open="(" separator="," close=")">
#{fid}
</foreach>
GROUP BY FID, DAYS_PAST_DUE) rp
GROUP BY rp.FID, rp.DAYS_PAST_DUE
AND r.reported_dt >= #{reportedDate}
)
SELECT
fid,
CASE WHEN MAX(CASE WHEN days_past_due > 90 THEN 1 ELSE 0 END) = 1
THEN 1 ELSE 0
END AS flag
FROM latest WHERE rn = 1
GROUP BY fid
WITH UR
</select>
</mapper>

View file

@ -12,14 +12,21 @@
</resultMap>
<select id="findFids" resultMap="fidsDataResultMap">
SELECT pn.NUMBER AS number, pn.FID AS fid, pn.FILE_SINCE_DT AS fileSinceDate
FROM PHONE_NORM pn
INNER JOIN NAME n ON n.FID = pn.FID
SELECT
pn.number,
pn.fid,
pn.file_since_dt
FROM indic.PHONE_NORM pn
JOIN (SELECT DISTINCT fid FROM indic.NAME) nf
ON nf.fid = pn.fid
WHERE number in
<foreach item="phone" collection="phones" open="(" separator="," close=")">
#{phone}
</foreach>
ORDER BY fileSinceDate DESC, fid DESC
ORDER BY
pn.number ASC,
pn.file_since_dt DESC,
pn.fid DESC
WITH UR
</select>
</mapper>

View file

@ -42,6 +42,17 @@
</foreach>
</update>
<update id="updatePhoneFid" parameterType="ru.nbch.credit_tracker.entities.dto.PhoneFidMap">
UPDATE signals_phone_fid_map
SET
fid = #{subject.fid},
flag_debt5000 = #{subject.flagDebt5000},
flag_90_12 = #{subject.flag90days},
calculated_at = #{subject.calculatedAt},
error_text = #{subject.errorText}
WHERE id = #{subject.id}
</update>
<update id="updatePhoneFidMapsErrors" parameterType="java.util.List">
<foreach collection="subjects" item="subject" separator=";">
UPDATE signals_phone_fid_map

View file

@ -0,0 +1,57 @@
package ru.nbch.credit_tracker.service;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import static org.mockito.Mockito.when;
import org.mockito.junit.jupiter.MockitoExtension;
import ru.nbch.credit_tracker.entities.dto.PhoneFidMap;
import ru.nbch.credit_tracker.mapper.indic.PhoneMapper;
import ru.nbch.credit_tracker.service.indic.PhoneService;
@ExtendWith(MockitoExtension.class)
public class PhoneServiceTest {
@Mock
PhoneMapper phoneMapper;
@Test
public void defineFidByPhoneTest() {
PhoneService phoneService = new PhoneService(phoneMapper);
fidsDataWithEqualSinceDt.sort(byDbOrder);
when(phoneMapper.findFids(Mockito.anyList())).thenReturn(fidsDataWithEqualSinceDt);
List<PhoneFidMap> phoneFidMap = new ArrayList<>();
phoneFidMap.add(PhoneFidMap.builder().phone("1111").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1112").build());
phoneFidMap.add(PhoneFidMap.builder().phone("1114").build());
Map<Integer, PhoneFidMap> res = phoneService.defineFidByPhone(phoneFidMap);
Assertions.assertEquals(3, res.size());
Assertions.assertEquals("1111", res.get(124).getPhone());
Assertions.assertEquals("1112", res.get(125).getPhone());
Assertions.assertEquals("1114", res.get(126).getPhone());
}
private final List<PhoneService.FidsData> fidsDataWithEqualSinceDt = new ArrayList<>(
List.of(
new PhoneService.FidsData(123, "1111", LocalDate.of(2025, 9, 10)),
new PhoneService.FidsData(124, "1111", LocalDate.of(2025, 9, 10)),
new PhoneService.FidsData(125, "1112", LocalDate.of(2024, 10, 10)),
new PhoneService.FidsData(126, "1114", LocalDate.of(2024, 10, 11))
)
);
Comparator<PhoneService.FidsData> byDbOrder =
Comparator.comparing(PhoneService.FidsData::getPhone, Comparator.nullsFirst(Comparator.naturalOrder())) // pn.number ASC
.thenComparing(PhoneService.FidsData::getFileSinceDate, Comparator.nullsLast(Comparator.reverseOrder())) // file_since_dt DESC
.thenComparing(PhoneService.FidsData::getFid, Comparator.reverseOrder());
}