From 38a95b7ec092c32f2c2083ebff82304b7270d601 Mon Sep 17 00:00:00 2001 From: "adel.ka" Date: Thu, 18 Sep 2025 16:31:15 +0300 Subject: [PATCH 1/5] SUPPORT-9416: listen application-status from kafka --- .../controller/UserApplicationController.java | 55 +++++---------- .../dao/UserApplicationListDao.java | 19 ++++++ .../kafka/ApplicationStatusListener.java | 57 ++++++++++++++++ .../kafka/model/ApplicationStatus.java | 14 ++++ .../model/StatusResponse.java | 6 ++ .../service/UserApplicationListService.java | 5 ++ .../websocket/dto/ProcessErrorMsg.java | 10 --- .../websocket/dto/ProcessResponseBody.java | 13 ---- .../websocket/dto/ProcessResponseDto.java | 11 --- .../websocket/enums/ClassName.java | 16 ----- .../component/button/UserManagementService.ts | 2 +- .../app/service/authorization.service.ts | 24 +------ .../app/service/status-update.service.ts | 68 +++++++++++++++---- .../app/websocket/websocket.service.ts | 37 ---------- 14 files changed, 176 insertions(+), 161 deletions(-) create mode 100644 backend/src/main/java/ru/micord/ervu/account_applications/kafka/ApplicationStatusListener.java create mode 100644 backend/src/main/java/ru/micord/ervu/account_applications/kafka/model/ApplicationStatus.java create mode 100644 backend/src/main/java/ru/micord/ervu/account_applications/model/StatusResponse.java delete mode 100644 backend/src/main/java/ru/micord/ervu/account_applications/websocket/dto/ProcessErrorMsg.java delete mode 100644 backend/src/main/java/ru/micord/ervu/account_applications/websocket/dto/ProcessResponseBody.java delete mode 100644 backend/src/main/java/ru/micord/ervu/account_applications/websocket/dto/ProcessResponseDto.java delete mode 100644 backend/src/main/java/ru/micord/ervu/account_applications/websocket/enums/ClassName.java delete mode 100644 frontend/src/ts/modules/app/websocket/websocket.service.ts diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/controller/UserApplicationController.java b/backend/src/main/java/ru/micord/ervu/account_applications/controller/UserApplicationController.java index a533865e..da341da6 100644 --- a/backend/src/main/java/ru/micord/ervu/account_applications/controller/UserApplicationController.java +++ b/backend/src/main/java/ru/micord/ervu/account_applications/controller/UserApplicationController.java @@ -1,58 +1,35 @@ package ru.micord.ervu.account_applications.controller; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.MediaType; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.PutMapping; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import ru.micord.ervu.account_applications.security.service.EncryptionService; +import ru.micord.ervu.account_applications.model.StatusResponse; import ru.micord.ervu.account_applications.service.UserApplicationListService; -import ru.micord.ervu.account_applications.websocket.dto.ProcessErrorMsg; -import ru.micord.ervu.account_applications.websocket.dto.ProcessResponseDto; /** * @author gulnaz */ @RestController public class UserApplicationController { - private static final Logger LOGGER = LoggerFactory.getLogger(UserApplicationController.class); - private final UserApplicationListService applicationService; - private final EncryptionService encryptionService; - public UserApplicationController(UserApplicationListService applicationService, - EncryptionService encryptionService) { + public UserApplicationController(UserApplicationListService applicationService) { this.applicationService = applicationService; - this.encryptionService = encryptionService; } - @PutMapping(value = "/status", consumes = MediaType.APPLICATION_JSON_VALUE) - public Long updateStatus(@RequestBody ProcessResponseDto data) { - Long appNumber = data.body().applicationNumber(); - switch (data.className()) { - case UPDATE -> { - LOGGER.info("update by appNumber = {}", appNumber); - String tempPass = data.body().tempPass(); - - if (StringUtils.hasText(tempPass)) { - String encryptedPassword = encryptionService.encrypt(tempPass); - applicationService.savePassword(appNumber, encryptedPassword); - } - else { - applicationService.saveAcceptedStatus(appNumber); - } - } - case PROCESS_ERROR -> { - ProcessErrorMsg errorMsg = data.body().msg(); - String msg = errorMsg == null ? "unknown error" : errorMsg.message(); - LOGGER.error("error by appNumber = {}, message: {}", appNumber, msg); - applicationService.saveError(appNumber, msg); - } - } - - return appNumber; + @PostMapping("/status/batch") + public List getBatchStatuses(@RequestBody List appNumbers) { + Map statuses = applicationService.getStatusesBatch(appNumbers); + return appNumbers.stream() + .map(appNumber -> new StatusResponse( + appNumber, + statuses.get(appNumber) + )) + .collect(Collectors.toList()); } } diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/dao/UserApplicationListDao.java b/backend/src/main/java/ru/micord/ervu/account_applications/dao/UserApplicationListDao.java index d905ab7a..41742d73 100644 --- a/backend/src/main/java/ru/micord/ervu/account_applications/dao/UserApplicationListDao.java +++ b/backend/src/main/java/ru/micord/ervu/account_applications/dao/UserApplicationListDao.java @@ -2,6 +2,9 @@ package ru.micord.ervu.account_applications.dao; import java.sql.Timestamp; import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import org.jooq.DSLContext; import org.springframework.stereotype.Repository; @@ -23,6 +26,22 @@ public class UserApplicationListDao { this.dslContext = dslContext; } + public Map getStatusesBatch(List appNumbers) { + if (appNumbers == null || appNumbers.isEmpty()) { + return Map.of(); + } + + return dslContext.select(USER_APPLICATION_LIST.NUMBER_APP, USER_APPLICATION_LIST.APPLICATION_STATUS) + .from(USER_APPLICATION_LIST) + .where(USER_APPLICATION_LIST.NUMBER_APP.in(appNumbers)) + .fetch() + .stream() + .collect(Collectors.toMap( + record -> record.get(USER_APPLICATION_LIST.NUMBER_APP), + record -> record.get(USER_APPLICATION_LIST.APPLICATION_STATUS) + )); + } + public void savePassword(Long appNumber, String encodedPass) { dslContext.update(USER_APPLICATION_LIST) .set(USER_APPLICATION_LIST.APPLICATION_STATUS, ACCEPTED.name()) diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/kafka/ApplicationStatusListener.java b/backend/src/main/java/ru/micord/ervu/account_applications/kafka/ApplicationStatusListener.java new file mode 100644 index 00000000..777e0252 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/account_applications/kafka/ApplicationStatusListener.java @@ -0,0 +1,57 @@ +package ru.micord.ervu.account_applications.kafka; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import ru.micord.ervu.account_applications.kafka.model.ApplicationStatus; +import ru.micord.ervu.account_applications.security.service.EncryptionService; +import ru.micord.ervu.account_applications.service.UserApplicationListService; + +/** + * @author Adel Kalimullin + */ +@Component +public class ApplicationStatusListener { + private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationStatusListener.class); + private final ObjectMapper mapper; + private final UserApplicationListService applicationService; + private final EncryptionService encryptionService; + + public ApplicationStatusListener(ObjectMapper mapper, + UserApplicationListService applicationService, EncryptionService encryptionService) { + this.mapper = mapper; + this.applicationService = applicationService; + this.encryptionService = encryptionService; + } + + @KafkaListener(id = "${kafka.application.status.group.id}", topics = "${kafka.application.status}") + public void listenKafkaDomain(String kafkaMessage) { + try { + ApplicationStatus applicationStatus = mapper.readValue(kafkaMessage, ApplicationStatus.class); + Long applicationNumber = applicationStatus.applicationNumber(); + if (applicationStatus.status()) { + LOGGER.info("update by appNumber = {}", applicationNumber); + String tempPass = applicationStatus.password(); + if (StringUtils.hasText(tempPass)) { + String encryptedPassword = encryptionService.encrypt(tempPass); + applicationService.savePassword(applicationNumber, encryptedPassword); + } + else { + applicationService.saveAcceptedStatus(applicationNumber); + } + } + else { + String errorMsg = applicationStatus.errorMsg(); + LOGGER.error("error by appNumber = {}, message: {}", applicationNumber, errorMsg); + applicationService.saveError(applicationNumber, errorMsg); + } + } + catch (JsonProcessingException e) { + LOGGER.error("Failed to deserialize message: {}", kafkaMessage, e); + } + } +} diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/kafka/model/ApplicationStatus.java b/backend/src/main/java/ru/micord/ervu/account_applications/kafka/model/ApplicationStatus.java new file mode 100644 index 00000000..5ef0a452 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/account_applications/kafka/model/ApplicationStatus.java @@ -0,0 +1,14 @@ +package ru.micord.ervu.account_applications.kafka.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Adel Kalimullin + */ +public record ApplicationStatus( + @JsonProperty("applicationNumber") Long applicationNumber, + @JsonProperty("password") String password, + @JsonProperty("status") boolean status, + @JsonProperty("userName") String userName, + @JsonProperty("description") String errorMsg +) {} \ No newline at end of file diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/model/StatusResponse.java b/backend/src/main/java/ru/micord/ervu/account_applications/model/StatusResponse.java new file mode 100644 index 00000000..1a9444bc --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/account_applications/model/StatusResponse.java @@ -0,0 +1,6 @@ +package ru.micord.ervu.account_applications.model; + +/** + * @author Adel Kalimullin + */ +public record StatusResponse(Long appNumber, String status) {} \ No newline at end of file diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/service/UserApplicationListService.java b/backend/src/main/java/ru/micord/ervu/account_applications/service/UserApplicationListService.java index 9735df52..96c27042 100644 --- a/backend/src/main/java/ru/micord/ervu/account_applications/service/UserApplicationListService.java +++ b/backend/src/main/java/ru/micord/ervu/account_applications/service/UserApplicationListService.java @@ -3,6 +3,7 @@ package ru.micord.ervu.account_applications.service; import java.sql.Timestamp; import java.util.Date; import java.util.List; +import java.util.Map; import org.springframework.stereotype.Service; import ru.micord.ervu.account_applications.component.dao.AuditDao; @@ -30,6 +31,10 @@ public class UserApplicationListService { this.securityContext = securityContext; } + public Map getStatusesBatch(List appNumbers) { + return dao.getStatusesBatch(appNumbers); + } + public void savePassword(Long appNumber, String encodedPass) { dao.savePassword(appNumber, encodedPass); saveAuditStatusByAppNumber(appNumber, ACCEPTED.name()); diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/websocket/dto/ProcessErrorMsg.java b/backend/src/main/java/ru/micord/ervu/account_applications/websocket/dto/ProcessErrorMsg.java deleted file mode 100644 index f6736468..00000000 --- a/backend/src/main/java/ru/micord/ervu/account_applications/websocket/dto/ProcessErrorMsg.java +++ /dev/null @@ -1,10 +0,0 @@ -package ru.micord.ervu.account_applications.websocket.dto; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * @author gulnaz - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public record ProcessErrorMsg(String message) { -} diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/websocket/dto/ProcessResponseBody.java b/backend/src/main/java/ru/micord/ervu/account_applications/websocket/dto/ProcessResponseBody.java deleted file mode 100644 index cbda2a0d..00000000 --- a/backend/src/main/java/ru/micord/ervu/account_applications/websocket/dto/ProcessResponseBody.java +++ /dev/null @@ -1,13 +0,0 @@ -package ru.micord.ervu.account_applications.websocket.dto; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * @author gulnaz - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public record ProcessResponseBody(String type, Long applicationNumber, String userName, - @JsonProperty("value") String tempPass, - String secretLink, ProcessErrorMsg msg) { -} diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/websocket/dto/ProcessResponseDto.java b/backend/src/main/java/ru/micord/ervu/account_applications/websocket/dto/ProcessResponseDto.java deleted file mode 100644 index c801403e..00000000 --- a/backend/src/main/java/ru/micord/ervu/account_applications/websocket/dto/ProcessResponseDto.java +++ /dev/null @@ -1,11 +0,0 @@ -package ru.micord.ervu.account_applications.websocket.dto; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import ru.micord.ervu.account_applications.websocket.enums.ClassName; - -/** - * @author gulnaz - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public record ProcessResponseDto(String forUser, ClassName className, ProcessResponseBody body) { -} diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/websocket/enums/ClassName.java b/backend/src/main/java/ru/micord/ervu/account_applications/websocket/enums/ClassName.java deleted file mode 100644 index 5eb35739..00000000 --- a/backend/src/main/java/ru/micord/ervu/account_applications/websocket/enums/ClassName.java +++ /dev/null @@ -1,16 +0,0 @@ -package ru.micord.ervu.account_applications.websocket.enums; - -import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * @author gulnaz - */ -public enum ClassName { - @JsonProperty("update") - UPDATE, - @JsonProperty("processError") - PROCESS_ERROR, - @JsonEnumDefaultValue - SKIP -} diff --git a/frontend/src/ts/account_applications/component/button/UserManagementService.ts b/frontend/src/ts/account_applications/component/button/UserManagementService.ts index 64a2df0d..76956e32 100644 --- a/frontend/src/ts/account_applications/component/button/UserManagementService.ts +++ b/frontend/src/ts/account_applications/component/button/UserManagementService.ts @@ -202,10 +202,10 @@ export class UserManagementService extends Behavior { this.httpClient.post(url, request).toPromise() .then((response: ProcessResponse) => { let code = response.code; - if (code !== '200') { this.saveError(appNumber, response.msg); } + this.statusUpdateService.trackApplication(appNumber); }) .catch(reason => { console.error("Error while executing request:", reason.toString()); diff --git a/frontend/src/ts/modules/app/service/authorization.service.ts b/frontend/src/ts/modules/app/service/authorization.service.ts index ca9086d8..57f83c27 100644 --- a/frontend/src/ts/modules/app/service/authorization.service.ts +++ b/frontend/src/ts/modules/app/service/authorization.service.ts @@ -1,9 +1,6 @@ import {Injectable, OnDestroy} from "@angular/core"; import {Subject} from "rxjs"; import {HttpClient} from "@angular/common/http"; -import {WebsocketService} from "../websocket/websocket.service"; -import {StatusUpdateService} from "./status-update.service"; -import {ErvuPermission} from "../enum/ErvuPermission"; export interface UserSession { userId: string, @@ -14,14 +11,13 @@ export interface UserSession { } @Injectable({providedIn: 'root'}) -export class AuthorizationService implements OnDestroy { +export class AuthorizationService{ private session: UserSession; public onSessionUpdate: Subject = new Subject(); - constructor(protected httpClient: HttpClient, protected websocketService: WebsocketService, - protected statusUpdateService: StatusUpdateService) {} + constructor(protected httpClient: HttpClient) {} public getCurrentSession(): Promise { if (this.session) return new Promise(resolve => resolve(this.session)); @@ -30,18 +26,6 @@ export class AuthorizationService implements OnDestroy { .then((session: UserSession) => { this.session = session; this.onSessionUpdate.next(session); - - if (this.hasPermission(ErvuPermission.APPROVER)) { - this.websocketService.subscribe(({data}) => { - let parsedObj = JSON.parse(data); - - if (parsedObj && parsedObj.body && parsedObj.body.applicationNumber) { - if (parsedObj.className === 'update' || parsedObj.className === 'processError') { - this.statusUpdateService.update(parsedObj); - } - } - }); - } return session; }) } @@ -77,8 +61,4 @@ export class AuthorizationService implements OnDestroy { getPermissions(): string[] { return this.isAuthorized() ? this.session.permissions : null; } - - ngOnDestroy(): void { - this.websocketService.unsubscribe(); - } } diff --git a/frontend/src/ts/modules/app/service/status-update.service.ts b/frontend/src/ts/modules/app/service/status-update.service.ts index 93034e00..7a23925d 100644 --- a/frontend/src/ts/modules/app/service/status-update.service.ts +++ b/frontend/src/ts/modules/app/service/status-update.service.ts @@ -2,9 +2,15 @@ import {Injectable} from "@angular/core"; import {BehaviorSubject} from "rxjs"; import {HttpClient} from "@angular/common/http"; +export interface StatusResponse { + appNumber: number; + status: string; +} + enum ApplicationStatus { AGREED = 'Согласована', - ACCEPTED = 'Исполнена' + ACCEPTED = 'Исполнена', + SENT = 'Отправлена' } @Injectable({providedIn: 'root'}) @@ -14,19 +20,57 @@ export class StatusUpdateService { } public statusMessage = new BehaviorSubject(null); + private pendingApplications = new Set(); + private pollingInterval: any; - public update(responseObj: any): void { - this.httpClient.put('status', responseObj) - .toPromise() - .then(appNumber => this.publishStatus(appNumber, responseObj.className === 'update')) - .catch(err => console.log('failed to update application status', err)); + public trackApplication(appNumber: number): void { + this.pendingApplications.add(appNumber); + this.startPolling(); } public publishStatus(appNumber: number, accepted: boolean) { - this.statusMessage.next( - { - appNumber: appNumber, - status: accepted ? ApplicationStatus.ACCEPTED : ApplicationStatus.AGREED - }); + this.statusMessage.next({ + appNumber: appNumber, + status: accepted ? ApplicationStatus.ACCEPTED : ApplicationStatus.AGREED + }); } -} + + private startPolling(): void { + if (this.pendingApplications.size === 0 || this.pollingInterval) return; + + this.pollingInterval = setInterval(() => { + this.checkPendingStatuses(); + }, 5000); + } + + private stopPolling(): void { + if (this.pollingInterval) { + clearInterval(this.pollingInterval); + this.pollingInterval = null; + } + } + + private checkPendingStatuses(): void { + const appNumbers = Array.from(this.pendingApplications); + if (appNumbers.length === 0) { + this.stopPolling(); + return; + } + + this.httpClient.post('status/batch', appNumbers) + .toPromise() + .then(responses => { + responses.forEach(response => { + if (response.status !== 'SENT') { + this.pendingApplications.delete(response.appNumber); + this.publishStatus(response.appNumber, response.status === 'ACCEPTED'); + } + }); + + if (this.pendingApplications.size === 0) { + this.stopPolling(); + } + }) + .catch(err => console.error('Failed to check statuses', err)); + } +} \ No newline at end of file diff --git a/frontend/src/ts/modules/app/websocket/websocket.service.ts b/frontend/src/ts/modules/app/websocket/websocket.service.ts deleted file mode 100644 index 41f68924..00000000 --- a/frontend/src/ts/modules/app/websocket/websocket.service.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {Injectable} from "@angular/core"; - -@Injectable({providedIn: 'root'}) -export class WebsocketService { - - private initialData; - - public subscribe(fn: Function): void { - let property = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data"); - const data = property.get; - this.initialData = data; - - // wrapper that replaces getter - function lookAtMessage() { - let socket = this.currentTarget instanceof WebSocket; - - if (!socket || !this.currentTarget.url.endsWith('notifier.message.send.push')) { - return data.call(this); - } - let msg = data.call(this); - Object.defineProperty(this, "data", { value: msg }); //anti-loop - fn({ data: msg, socket: this.currentTarget, event: this }); - return msg; - } - property.get = lookAtMessage; - Object.defineProperty(MessageEvent.prototype, "data", property); - } - - public unsubscribe(): void { - let property = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data"); - - if (this.initialData) { - property.get = this.initialData; - Object.defineProperty(MessageEvent.prototype, "data", property); - } - } -} From dc9eec7fda788b2bbe8da070ec32cdbf3bf39aa4 Mon Sep 17 00:00:00 2001 From: "adel.ka" Date: Thu, 18 Sep 2025 17:36:02 +0300 Subject: [PATCH 2/5] SUPPORT-9416: fix --- .../rpc/UserApplicationListRpcService.java | 12 +++--- .../kafka/ApplicationStatusListener.java | 24 +++++++++--- .../service/UserApplicationListService.java | 23 ++++------- .../component/button/UserManagementService.ts | 2 +- .../app/service/authorization.service.ts | 2 +- .../app/service/status-update.service.ts | 39 +++++++++++++++---- 6 files changed, 66 insertions(+), 36 deletions(-) diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/component/rpc/UserApplicationListRpcService.java b/backend/src/main/java/ru/micord/ervu/account_applications/component/rpc/UserApplicationListRpcService.java index 1b8cb27d..cfcefb68 100644 --- a/backend/src/main/java/ru/micord/ervu/account_applications/component/rpc/UserApplicationListRpcService.java +++ b/backend/src/main/java/ru/micord/ervu/account_applications/component/rpc/UserApplicationListRpcService.java @@ -6,6 +6,8 @@ import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import ru.micord.ervu.account_applications.security.context.SecurityContext; +import ru.micord.ervu.account_applications.security.model.UserSession; import ru.micord.ervu.account_applications.service.AccountFetchService; import ru.micord.ervu.account_applications.service.UserApplicationListService; @@ -24,16 +26,14 @@ public class UserApplicationListRpcService extends Behavior { private UserApplicationListService applicationListService; @Autowired private AccountFetchService accountService; + @Autowired + private SecurityContext securityContext; @RpcCall public void saveError(long appNumber, String errorMsg) { + UserSession userSession = securityContext.getUserSession(); LOGGER.error("error for application = {}, message: {}", appNumber, errorMsg); - applicationListService.saveError(appNumber, errorMsg); - } - - @RpcCall - public void saveAcceptedStatus(long appNumber) { - applicationListService.saveAcceptedStatus(appNumber); + applicationListService.saveError(appNumber, errorMsg, userSession.name(), userSession.userId()); } @RpcCall diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/kafka/ApplicationStatusListener.java b/backend/src/main/java/ru/micord/ervu/account_applications/kafka/ApplicationStatusListener.java index 777e0252..38e8459f 100644 --- a/backend/src/main/java/ru/micord/ervu/account_applications/kafka/ApplicationStatusListener.java +++ b/backend/src/main/java/ru/micord/ervu/account_applications/kafka/ApplicationStatusListener.java @@ -5,10 +5,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.messaging.handler.annotation.Header; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import ru.micord.ervu.account_applications.kafka.model.ApplicationStatus; +import ru.micord.ervu.account_applications.security.model.jwt.UserClaims; import ru.micord.ervu.account_applications.security.service.EncryptionService; +import ru.micord.ervu.account_applications.security.service.JwtTokenService; import ru.micord.ervu.account_applications.service.UserApplicationListService; /** @@ -20,17 +23,26 @@ public class ApplicationStatusListener { private final ObjectMapper mapper; private final UserApplicationListService applicationService; private final EncryptionService encryptionService; + private final JwtTokenService jwtTokenService; public ApplicationStatusListener(ObjectMapper mapper, - UserApplicationListService applicationService, EncryptionService encryptionService) { + UserApplicationListService applicationService, EncryptionService encryptionService, + JwtTokenService jwtTokenService) { this.mapper = mapper; this.applicationService = applicationService; this.encryptionService = encryptionService; + this.jwtTokenService = jwtTokenService; } - @KafkaListener(id = "${kafka.application.status.group.id}", topics = "${kafka.application.status}") - public void listenKafkaDomain(String kafkaMessage) { + @KafkaListener(id = "${kafka.application.status.group.id}", + topics = "${kafka.application.status}") + public void listenKafkaDomain(String kafkaMessage, @Header("authorization") String authHeader) { try { + String token = authHeader.replace("Bearer ", ""); + UserClaims userClaims = jwtTokenService.getUserClaims(token); + String name = userClaims.name(); + String userId = userClaims.userId(); + ApplicationStatus applicationStatus = mapper.readValue(kafkaMessage, ApplicationStatus.class); Long applicationNumber = applicationStatus.applicationNumber(); if (applicationStatus.status()) { @@ -38,16 +50,16 @@ public class ApplicationStatusListener { String tempPass = applicationStatus.password(); if (StringUtils.hasText(tempPass)) { String encryptedPassword = encryptionService.encrypt(tempPass); - applicationService.savePassword(applicationNumber, encryptedPassword); + applicationService.savePassword(applicationNumber, encryptedPassword, name, userId); } else { - applicationService.saveAcceptedStatus(applicationNumber); + applicationService.saveAcceptedStatus(applicationNumber, name, userId); } } else { String errorMsg = applicationStatus.errorMsg(); LOGGER.error("error by appNumber = {}, message: {}", applicationNumber, errorMsg); - applicationService.saveError(applicationNumber, errorMsg); + applicationService.saveError(applicationNumber, errorMsg, name, userId); } } catch (JsonProcessingException e) { diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/service/UserApplicationListService.java b/backend/src/main/java/ru/micord/ervu/account_applications/service/UserApplicationListService.java index 96c27042..345dc1f1 100644 --- a/backend/src/main/java/ru/micord/ervu/account_applications/service/UserApplicationListService.java +++ b/backend/src/main/java/ru/micord/ervu/account_applications/service/UserApplicationListService.java @@ -8,8 +8,6 @@ import java.util.Map; import org.springframework.stereotype.Service; import ru.micord.ervu.account_applications.component.dao.AuditDao; import ru.micord.ervu.account_applications.dao.UserApplicationListDao; -import ru.micord.ervu.account_applications.security.context.SecurityContext; -import ru.micord.ervu.account_applications.security.model.UserSession; import utils.DateTimeUtil; import static ru.micord.ervu.account_applications.enums.ApplicationStatus.ACCEPTED; @@ -23,42 +21,37 @@ public class UserApplicationListService { private final UserApplicationListDao dao; private final AuditDao auditDao; - private final SecurityContext securityContext; - public UserApplicationListService(UserApplicationListDao dao, AuditDao auditDao, SecurityContext securityContext) { + public UserApplicationListService(UserApplicationListDao dao, AuditDao auditDao) { this.dao = dao; this.auditDao = auditDao; - this.securityContext = securityContext; } public Map getStatusesBatch(List appNumbers) { return dao.getStatusesBatch(appNumbers); } - public void savePassword(Long appNumber, String encodedPass) { + public void savePassword(Long appNumber, String encodedPass, String name, String userId) { dao.savePassword(appNumber, encodedPass); - saveAuditStatusByAppNumber(appNumber, ACCEPTED.name()); + saveAuditStatusByAppNumber(appNumber, ACCEPTED.name(), name, userId); } public boolean userExists(String login){ return dao.userExists(login); } - public void saveAcceptedStatus(long appNumber) { + public void saveAcceptedStatus(long appNumber, String name, String userId) { dao.saveAcceptedStatus(appNumber); - saveAuditStatusByAppNumber(appNumber, ACCEPTED.name()); + saveAuditStatusByAppNumber(appNumber, ACCEPTED.name(), name, userId); } - public void saveError(long appNumber, String errorMsg) { + public void saveError(long appNumber, String errorMsg, String name, String userId) { dao.saveError(appNumber, errorMsg); - saveAuditStatusByAppNumber(appNumber, AGREED.name()); + saveAuditStatusByAppNumber(appNumber, AGREED.name(), name, userId); } - private void saveAuditStatusByAppNumber(long appNumber, String status) { + private void saveAuditStatusByAppNumber(long appNumber, String status, String name, String userId) { List appIds = auditDao.selectAppListIdsByAppNumber(appNumber); - UserSession userSession = securityContext.getUserSession(); - String name = userSession.name(); - String userId = userSession.userId(); appIds.forEach(id -> { auditDao.insert(id, name, userId, status, Timestamp.valueOf( DateTimeUtil.dateToLocalDateTimeUtc(new Date()))); diff --git a/frontend/src/ts/account_applications/component/button/UserManagementService.ts b/frontend/src/ts/account_applications/component/button/UserManagementService.ts index 76956e32..eb742280 100644 --- a/frontend/src/ts/account_applications/component/button/UserManagementService.ts +++ b/frontend/src/ts/account_applications/component/button/UserManagementService.ts @@ -7,7 +7,7 @@ import { TextField, Visible } from "@webbpm/base-package"; -import {HttpClient, HttpErrorResponse, HttpResponse} from "@angular/common/http"; +import {HttpClient} from "@angular/common/http"; import {FormField} from "../field/FormField"; import {AuthorizationService} from "../../../modules/app/service/authorization.service"; import {ApplicationKind} from "../enum/ApplicationKind"; diff --git a/frontend/src/ts/modules/app/service/authorization.service.ts b/frontend/src/ts/modules/app/service/authorization.service.ts index 57f83c27..d6be7356 100644 --- a/frontend/src/ts/modules/app/service/authorization.service.ts +++ b/frontend/src/ts/modules/app/service/authorization.service.ts @@ -1,4 +1,4 @@ -import {Injectable, OnDestroy} from "@angular/core"; +import {Injectable} from "@angular/core"; import {Subject} from "rxjs"; import {HttpClient} from "@angular/common/http"; diff --git a/frontend/src/ts/modules/app/service/status-update.service.ts b/frontend/src/ts/modules/app/service/status-update.service.ts index 7a23925d..ddc8a1da 100644 --- a/frontend/src/ts/modules/app/service/status-update.service.ts +++ b/frontend/src/ts/modules/app/service/status-update.service.ts @@ -20,23 +20,30 @@ export class StatusUpdateService { } public statusMessage = new BehaviorSubject(null); - private pendingApplications = new Set(); + private pendingApplications = new Map(); private pollingInterval: any; + private readonly MAX_ATTEMPTS = 12; public trackApplication(appNumber: number): void { - this.pendingApplications.add(appNumber); - this.startPolling(); + if (!this.pendingApplications.has(appNumber)) { + this.pendingApplications.set(appNumber, 0); + this.startPolling(); + } } public publishStatus(appNumber: number, accepted: boolean) { this.statusMessage.next({ appNumber: appNumber, - status: accepted ? ApplicationStatus.ACCEPTED : ApplicationStatus.AGREED + status: accepted + ? ApplicationStatus.ACCEPTED + : ApplicationStatus.AGREED }); } private startPolling(): void { - if (this.pendingApplications.size === 0 || this.pollingInterval) return; + if (this.pendingApplications.size === 0 || this.pollingInterval) { + return; + } this.pollingInterval = setInterval(() => { this.checkPendingStatuses(); @@ -51,7 +58,7 @@ export class StatusUpdateService { } private checkPendingStatuses(): void { - const appNumbers = Array.from(this.pendingApplications); + const appNumbers = Array.from(this.pendingApplications.keys()); if (appNumbers.length === 0) { this.stopPolling(); return; @@ -61,16 +68,34 @@ export class StatusUpdateService { .toPromise() .then(responses => { responses.forEach(response => { + const attemptCount = (this.pendingApplications.get(response.appNumber) || 0) + 1; + this.pendingApplications.set(response.appNumber, attemptCount); + if (response.status !== 'SENT') { this.pendingApplications.delete(response.appNumber); this.publishStatus(response.appNumber, response.status === 'ACCEPTED'); } + else if (attemptCount >= this.MAX_ATTEMPTS) { + this.pendingApplications.delete(response.appNumber); + console.warn(`Max attempts exceeded for application ${response.appNumber}`); + } }); if (this.pendingApplications.size === 0) { this.stopPolling(); } }) - .catch(err => console.error('Failed to check statuses', err)); + .catch(err => { + console.error('Failed to check statuses', err); + appNumbers.forEach(appNumber => { + const attemptCount = (this.pendingApplications.get(appNumber) || 0) + 1; + this.pendingApplications.set(appNumber, attemptCount); + + if (attemptCount >= this.MAX_ATTEMPTS) { + this.pendingApplications.delete(appNumber); + console.warn(`Max attempts exceeded for application ${appNumber} due to errors`); + } + }); + }); } } \ No newline at end of file From 9fc4d612692de1c9e180b4e0d13fd029e0bbb04c Mon Sep 17 00:00:00 2001 From: "adel.ka" Date: Fri, 19 Sep 2025 09:30:00 +0300 Subject: [PATCH 3/5] SUPPORT-9416: fix --- frontend/src/ts/modules/app/service/status-update.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/ts/modules/app/service/status-update.service.ts b/frontend/src/ts/modules/app/service/status-update.service.ts index ddc8a1da..dc16330c 100644 --- a/frontend/src/ts/modules/app/service/status-update.service.ts +++ b/frontend/src/ts/modules/app/service/status-update.service.ts @@ -9,8 +9,7 @@ export interface StatusResponse { enum ApplicationStatus { AGREED = 'Согласована', - ACCEPTED = 'Исполнена', - SENT = 'Отправлена' + ACCEPTED = 'Исполнена' } @Injectable({providedIn: 'root'}) From cf09440280afbec60101d6231153759eee6bdc9f Mon Sep 17 00:00:00 2001 From: "adel.ka" Date: Mon, 22 Sep 2025 09:21:32 +0300 Subject: [PATCH 4/5] SUPPORT-9416: fix from review --- .../kafka/ApplicationStatusListener.java | 12 ++++++------ ...ApplicationStatus.java => DeclarationStatus.java} | 2 +- .../service/UserApplicationListService.java | 4 ++++ 3 files changed, 11 insertions(+), 7 deletions(-) rename backend/src/main/java/ru/micord/ervu/account_applications/kafka/model/{ApplicationStatus.java => DeclarationStatus.java} (92%) diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/kafka/ApplicationStatusListener.java b/backend/src/main/java/ru/micord/ervu/account_applications/kafka/ApplicationStatusListener.java index 38e8459f..98b0bbd6 100644 --- a/backend/src/main/java/ru/micord/ervu/account_applications/kafka/ApplicationStatusListener.java +++ b/backend/src/main/java/ru/micord/ervu/account_applications/kafka/ApplicationStatusListener.java @@ -8,7 +8,7 @@ import org.springframework.kafka.annotation.KafkaListener; import org.springframework.messaging.handler.annotation.Header; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; -import ru.micord.ervu.account_applications.kafka.model.ApplicationStatus; +import ru.micord.ervu.account_applications.kafka.model.DeclarationStatus; import ru.micord.ervu.account_applications.security.model.jwt.UserClaims; import ru.micord.ervu.account_applications.security.service.EncryptionService; import ru.micord.ervu.account_applications.security.service.JwtTokenService; @@ -43,11 +43,11 @@ public class ApplicationStatusListener { String name = userClaims.name(); String userId = userClaims.userId(); - ApplicationStatus applicationStatus = mapper.readValue(kafkaMessage, ApplicationStatus.class); - Long applicationNumber = applicationStatus.applicationNumber(); - if (applicationStatus.status()) { + DeclarationStatus declarationStatus = mapper.readValue(kafkaMessage, DeclarationStatus.class); + Long applicationNumber = declarationStatus.applicationNumber(); + if (declarationStatus.status()) { LOGGER.info("update by appNumber = {}", applicationNumber); - String tempPass = applicationStatus.password(); + String tempPass = declarationStatus.password(); if (StringUtils.hasText(tempPass)) { String encryptedPassword = encryptionService.encrypt(tempPass); applicationService.savePassword(applicationNumber, encryptedPassword, name, userId); @@ -57,7 +57,7 @@ public class ApplicationStatusListener { } } else { - String errorMsg = applicationStatus.errorMsg(); + String errorMsg = declarationStatus.errorMsg(); LOGGER.error("error by appNumber = {}, message: {}", applicationNumber, errorMsg); applicationService.saveError(applicationNumber, errorMsg, name, userId); } diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/kafka/model/ApplicationStatus.java b/backend/src/main/java/ru/micord/ervu/account_applications/kafka/model/DeclarationStatus.java similarity index 92% rename from backend/src/main/java/ru/micord/ervu/account_applications/kafka/model/ApplicationStatus.java rename to backend/src/main/java/ru/micord/ervu/account_applications/kafka/model/DeclarationStatus.java index 5ef0a452..c02e65a6 100644 --- a/backend/src/main/java/ru/micord/ervu/account_applications/kafka/model/ApplicationStatus.java +++ b/backend/src/main/java/ru/micord/ervu/account_applications/kafka/model/DeclarationStatus.java @@ -5,7 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; /** * @author Adel Kalimullin */ -public record ApplicationStatus( +public record DeclarationStatus( @JsonProperty("applicationNumber") Long applicationNumber, @JsonProperty("password") String password, @JsonProperty("status") boolean status, diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/service/UserApplicationListService.java b/backend/src/main/java/ru/micord/ervu/account_applications/service/UserApplicationListService.java index 345dc1f1..c12c45c9 100644 --- a/backend/src/main/java/ru/micord/ervu/account_applications/service/UserApplicationListService.java +++ b/backend/src/main/java/ru/micord/ervu/account_applications/service/UserApplicationListService.java @@ -6,6 +6,7 @@ import java.util.List; import java.util.Map; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import ru.micord.ervu.account_applications.component.dao.AuditDao; import ru.micord.ervu.account_applications.dao.UserApplicationListDao; import utils.DateTimeUtil; @@ -31,6 +32,7 @@ public class UserApplicationListService { return dao.getStatusesBatch(appNumbers); } + @Transactional public void savePassword(Long appNumber, String encodedPass, String name, String userId) { dao.savePassword(appNumber, encodedPass); saveAuditStatusByAppNumber(appNumber, ACCEPTED.name(), name, userId); @@ -40,11 +42,13 @@ public class UserApplicationListService { return dao.userExists(login); } + @Transactional public void saveAcceptedStatus(long appNumber, String name, String userId) { dao.saveAcceptedStatus(appNumber); saveAuditStatusByAppNumber(appNumber, ACCEPTED.name(), name, userId); } + @Transactional public void saveError(long appNumber, String errorMsg, String name, String userId) { dao.saveError(appNumber, errorMsg); saveAuditStatusByAppNumber(appNumber, AGREED.name(), name, userId); From 72976e5f409e8798ccc867b07b7297d8fa3132d8 Mon Sep 17 00:00:00 2001 From: "adel.ka" Date: Mon, 22 Sep 2025 09:29:02 +0300 Subject: [PATCH 5/5] SUPPORT-9416: fix from review --- ...er.java => DeclarationStatusListener.java} | 6 ++-- .../component/button/UserManagementService.ts | 4 ++- .../app/service/status-update.service.ts | 32 +++++++++---------- 3 files changed, 22 insertions(+), 20 deletions(-) rename backend/src/main/java/ru/micord/ervu/account_applications/kafka/{ApplicationStatusListener.java => DeclarationStatusListener.java} (93%) diff --git a/backend/src/main/java/ru/micord/ervu/account_applications/kafka/ApplicationStatusListener.java b/backend/src/main/java/ru/micord/ervu/account_applications/kafka/DeclarationStatusListener.java similarity index 93% rename from backend/src/main/java/ru/micord/ervu/account_applications/kafka/ApplicationStatusListener.java rename to backend/src/main/java/ru/micord/ervu/account_applications/kafka/DeclarationStatusListener.java index 98b0bbd6..da60ac97 100644 --- a/backend/src/main/java/ru/micord/ervu/account_applications/kafka/ApplicationStatusListener.java +++ b/backend/src/main/java/ru/micord/ervu/account_applications/kafka/DeclarationStatusListener.java @@ -18,14 +18,14 @@ import ru.micord.ervu.account_applications.service.UserApplicationListService; * @author Adel Kalimullin */ @Component -public class ApplicationStatusListener { - private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationStatusListener.class); +public class DeclarationStatusListener { + private static final Logger LOGGER = LoggerFactory.getLogger(DeclarationStatusListener.class); private final ObjectMapper mapper; private final UserApplicationListService applicationService; private final EncryptionService encryptionService; private final JwtTokenService jwtTokenService; - public ApplicationStatusListener(ObjectMapper mapper, + public DeclarationStatusListener(ObjectMapper mapper, UserApplicationListService applicationService, EncryptionService encryptionService, JwtTokenService jwtTokenService) { this.mapper = mapper; diff --git a/frontend/src/ts/account_applications/component/button/UserManagementService.ts b/frontend/src/ts/account_applications/component/button/UserManagementService.ts index eb742280..a7b063af 100644 --- a/frontend/src/ts/account_applications/component/button/UserManagementService.ts +++ b/frontend/src/ts/account_applications/component/button/UserManagementService.ts @@ -205,7 +205,9 @@ export class UserManagementService extends Behavior { if (code !== '200') { this.saveError(appNumber, response.msg); } - this.statusUpdateService.trackApplication(appNumber); + else { + this.statusUpdateService.trackDeclaration(appNumber); + } }) .catch(reason => { console.error("Error while executing request:", reason.toString()); diff --git a/frontend/src/ts/modules/app/service/status-update.service.ts b/frontend/src/ts/modules/app/service/status-update.service.ts index dc16330c..321c8e65 100644 --- a/frontend/src/ts/modules/app/service/status-update.service.ts +++ b/frontend/src/ts/modules/app/service/status-update.service.ts @@ -19,13 +19,13 @@ export class StatusUpdateService { } public statusMessage = new BehaviorSubject(null); - private pendingApplications = new Map(); + private pendingDeclarations = new Map(); private pollingInterval: any; private readonly MAX_ATTEMPTS = 12; - public trackApplication(appNumber: number): void { - if (!this.pendingApplications.has(appNumber)) { - this.pendingApplications.set(appNumber, 0); + public trackDeclaration(appNumber: number): void { + if (!this.pendingDeclarations.has(appNumber)) { + this.pendingDeclarations.set(appNumber, 0); this.startPolling(); } } @@ -40,7 +40,7 @@ export class StatusUpdateService { } private startPolling(): void { - if (this.pendingApplications.size === 0 || this.pollingInterval) { + if (this.pendingDeclarations.size === 0 || this.pollingInterval) { return; } @@ -57,7 +57,7 @@ export class StatusUpdateService { } private checkPendingStatuses(): void { - const appNumbers = Array.from(this.pendingApplications.keys()); + const appNumbers = Array.from(this.pendingDeclarations.keys()); if (appNumbers.length === 0) { this.stopPolling(); return; @@ -67,32 +67,32 @@ export class StatusUpdateService { .toPromise() .then(responses => { responses.forEach(response => { - const attemptCount = (this.pendingApplications.get(response.appNumber) || 0) + 1; - this.pendingApplications.set(response.appNumber, attemptCount); + const attemptCount = (this.pendingDeclarations.get(response.appNumber) || 0) + 1; + this.pendingDeclarations.set(response.appNumber, attemptCount); if (response.status !== 'SENT') { - this.pendingApplications.delete(response.appNumber); + this.pendingDeclarations.delete(response.appNumber); this.publishStatus(response.appNumber, response.status === 'ACCEPTED'); } else if (attemptCount >= this.MAX_ATTEMPTS) { - this.pendingApplications.delete(response.appNumber); - console.warn(`Max attempts exceeded for application ${response.appNumber}`); + this.pendingDeclarations.delete(response.appNumber); + console.warn(`Max attempts exceeded for declaration ${response.appNumber}`); } }); - if (this.pendingApplications.size === 0) { + if (this.pendingDeclarations.size === 0) { this.stopPolling(); } }) .catch(err => { console.error('Failed to check statuses', err); appNumbers.forEach(appNumber => { - const attemptCount = (this.pendingApplications.get(appNumber) || 0) + 1; - this.pendingApplications.set(appNumber, attemptCount); + const attemptCount = (this.pendingDeclarations.get(appNumber) || 0) + 1; + this.pendingDeclarations.set(appNumber, attemptCount); if (attemptCount >= this.MAX_ATTEMPTS) { - this.pendingApplications.delete(appNumber); - console.warn(`Max attempts exceeded for application ${appNumber} due to errors`); + this.pendingDeclarations.delete(appNumber); + console.warn(`Max attempts exceeded for declaration ${appNumber} due to errors`); } }); });