diff --git a/backend/src/main/java/ervu/enums/ExcerptStatus.java b/backend/src/main/java/ervu/enums/ExcerptStatus.java new file mode 100644 index 00000000..4b4cc94e --- /dev/null +++ b/backend/src/main/java/ervu/enums/ExcerptStatus.java @@ -0,0 +1,14 @@ +package ervu.enums; + +import ru.cg.webbpm.modules.webkit.annotations.Model; + +/** + * @author gulnaz + */ +@Model +public enum ExcerptStatus { + NONE, + PENDING, + READY, + NOT_FOUND +} diff --git a/backend/src/main/java/ervu_lkrp_ul/ervu_lkrp_ul/db_beans/public_/Public.java b/backend/src/main/java/ervu_lkrp_ul/ervu_lkrp_ul/db_beans/public_/Public.java index 5d20db39..98fa6116 100644 --- a/backend/src/main/java/ervu_lkrp_ul/ervu_lkrp_ul/db_beans/public_/Public.java +++ b/backend/src/main/java/ervu_lkrp_ul/ervu_lkrp_ul/db_beans/public_/Public.java @@ -7,6 +7,7 @@ package ervu_lkrp_ul.ervu_lkrp_ul.db_beans.public_; import ervu_lkrp_ul.ervu_lkrp_ul.db_beans.DefaultCatalog; import ervu_lkrp_ul.ervu_lkrp_ul.db_beans.public_.tables.Databasechangelog; import ervu_lkrp_ul.ervu_lkrp_ul.db_beans.public_.tables.Databasechangeloglock; +import ervu_lkrp_ul.ervu_lkrp_ul.db_beans.public_.tables.ExcerptHistory; import ervu_lkrp_ul.ervu_lkrp_ul.db_beans.public_.tables.InteractionLog; import ervu_lkrp_ul.ervu_lkrp_ul.db_beans.public_.tables.OkopfRecords; import ervu_lkrp_ul.ervu_lkrp_ul.db_beans.public_.tables.OrganizationAllowed; @@ -43,6 +44,11 @@ public class Public extends SchemaImpl { */ public final Databasechangeloglock DATABASECHANGELOGLOCK = Databasechangeloglock.DATABASECHANGELOGLOCK; + /** + * The table public.excerpt_history. + */ + public final ExcerptHistory EXCERPT_HISTORY = ExcerptHistory.EXCERPT_HISTORY; + /** * The table public.interaction_log. */ @@ -81,6 +87,7 @@ public class Public extends SchemaImpl { return Arrays.asList( Databasechangelog.DATABASECHANGELOG, Databasechangeloglock.DATABASECHANGELOGLOCK, + ExcerptHistory.EXCERPT_HISTORY, InteractionLog.INTERACTION_LOG, OkopfRecords.OKOPF_RECORDS, OrganizationAllowed.ORGANIZATION_ALLOWED, diff --git a/backend/src/main/java/ervu_lkrp_ul/ervu_lkrp_ul/db_beans/public_/Tables.java b/backend/src/main/java/ervu_lkrp_ul/ervu_lkrp_ul/db_beans/public_/Tables.java index 37305e26..84486edb 100644 --- a/backend/src/main/java/ervu_lkrp_ul/ervu_lkrp_ul/db_beans/public_/Tables.java +++ b/backend/src/main/java/ervu_lkrp_ul/ervu_lkrp_ul/db_beans/public_/Tables.java @@ -6,6 +6,7 @@ package ervu_lkrp_ul.ervu_lkrp_ul.db_beans.public_; import ervu_lkrp_ul.ervu_lkrp_ul.db_beans.public_.tables.Databasechangelog; import ervu_lkrp_ul.ervu_lkrp_ul.db_beans.public_.tables.Databasechangeloglock; +import ervu_lkrp_ul.ervu_lkrp_ul.db_beans.public_.tables.ExcerptHistory; import ervu_lkrp_ul.ervu_lkrp_ul.db_beans.public_.tables.InteractionLog; import ervu_lkrp_ul.ervu_lkrp_ul.db_beans.public_.tables.OkopfRecords; import ervu_lkrp_ul.ervu_lkrp_ul.db_beans.public_.tables.OrganizationAllowed; @@ -28,6 +29,11 @@ public class Tables { */ public static final Databasechangeloglock DATABASECHANGELOGLOCK = Databasechangeloglock.DATABASECHANGELOGLOCK; + /** + * The table public.excerpt_history. + */ + public static final ExcerptHistory EXCERPT_HISTORY = ExcerptHistory.EXCERPT_HISTORY; + /** * The table public.interaction_log. */ diff --git a/backend/src/main/java/ervu_lkrp_ul/ervu_lkrp_ul/db_beans/public_/tables/ExcerptHistory.java b/backend/src/main/java/ervu_lkrp_ul/ervu_lkrp_ul/db_beans/public_/tables/ExcerptHistory.java new file mode 100644 index 00000000..81c89535 --- /dev/null +++ b/backend/src/main/java/ervu_lkrp_ul/ervu_lkrp_ul/db_beans/public_/tables/ExcerptHistory.java @@ -0,0 +1,243 @@ +/* + * This file is generated by jOOQ. + */ +package ervu_lkrp_ul.ervu_lkrp_ul.db_beans.public_.tables; + + +import ervu_lkrp_ul.ervu_lkrp_ul.db_beans.public_.Public; +import ervu_lkrp_ul.ervu_lkrp_ul.db_beans.public_.tables.records.ExcerptHistoryRecord; + +import java.sql.Timestamp; +import java.util.Collection; + +import org.jooq.Condition; +import org.jooq.Field; +import org.jooq.Identity; +import org.jooq.Name; +import org.jooq.PlainSQL; +import org.jooq.QueryPart; +import org.jooq.SQL; +import org.jooq.Schema; +import org.jooq.Select; +import org.jooq.Stringly; +import org.jooq.Table; +import org.jooq.TableField; +import org.jooq.TableOptions; +import org.jooq.impl.DSL; +import org.jooq.impl.SQLDataType; +import org.jooq.impl.TableImpl; + + +/** + * This class is generated by jOOQ. + */ +@SuppressWarnings({ "all", "unchecked", "rawtypes" }) +public class ExcerptHistory extends TableImpl { + + private static final long serialVersionUID = 1L; + + /** + * The reference instance of public.excerpt_history + */ + public static final ExcerptHistory EXCERPT_HISTORY = new ExcerptHistory(); + + /** + * The class holding records for this type + */ + @Override + public Class getRecordType() { + return ExcerptHistoryRecord.class; + } + + /** + * The column public.excerpt_history.id. + */ + public final TableField ID = createField(DSL.name("id"), SQLDataType.BIGINT.nullable(false).identity(true), this, ""); + + /** + * The column public.excerpt_history.ervu_id. + */ + public final TableField ERVU_ID = createField(DSL.name("ervu_id"), SQLDataType.VARCHAR(36).nullable(false), this, ""); + + /** + * The column public.excerpt_history.prn_oid. + */ + public final TableField PRN_OID = createField(DSL.name("prn_oid"), SQLDataType.VARCHAR(10).nullable(false), this, ""); + + /** + * The column public.excerpt_history.url. + */ + public final TableField URL = createField(DSL.name("url"), SQLDataType.CLOB, this, ""); + + /** + * The column public.excerpt_history.datetime. + */ + public final TableField DATETIME = createField(DSL.name("datetime"), SQLDataType.TIMESTAMP(0).nullable(false).defaultValue(DSL.field(DSL.raw("now()"), SQLDataType.TIMESTAMP)), this, ""); + + /** + * The column public.excerpt_history.status. + */ + public final TableField STATUS = createField(DSL.name("status"), SQLDataType.VARCHAR(20), this, ""); + + private ExcerptHistory(Name alias, Table aliased) { + this(alias, aliased, (Field[]) null, null); + } + + private ExcerptHistory(Name alias, Table aliased, Field[] parameters, Condition where) { + super(alias, null, aliased, parameters, DSL.comment(""), TableOptions.table(), where); + } + + /** + * Create an aliased public.excerpt_history table reference + */ + public ExcerptHistory(String alias) { + this(DSL.name(alias), EXCERPT_HISTORY); + } + + /** + * Create an aliased public.excerpt_history table reference + */ + public ExcerptHistory(Name alias) { + this(alias, EXCERPT_HISTORY); + } + + /** + * Create a public.excerpt_history table reference + */ + public ExcerptHistory() { + this(DSL.name("excerpt_history"), null); + } + + @Override + public Schema getSchema() { + return aliased() ? null : Public.PUBLIC; + } + + @Override + public Identity getIdentity() { + return (Identity) super.getIdentity(); + } + + @Override + public ExcerptHistory as(String alias) { + return new ExcerptHistory(DSL.name(alias), this); + } + + @Override + public ExcerptHistory as(Name alias) { + return new ExcerptHistory(alias, this); + } + + @Override + public ExcerptHistory as(Table alias) { + return new ExcerptHistory(alias.getQualifiedName(), this); + } + + /** + * Rename this table + */ + @Override + public ExcerptHistory rename(String name) { + return new ExcerptHistory(DSL.name(name), null); + } + + /** + * Rename this table + */ + @Override + public ExcerptHistory rename(Name name) { + return new ExcerptHistory(name, null); + } + + /** + * Rename this table + */ + @Override + public ExcerptHistory rename(Table name) { + return new ExcerptHistory(name.getQualifiedName(), null); + } + + /** + * Create an inline derived table from this table + */ + @Override + public ExcerptHistory where(Condition condition) { + return new ExcerptHistory(getQualifiedName(), aliased() ? this : null, null, condition); + } + + /** + * Create an inline derived table from this table + */ + @Override + public ExcerptHistory where(Collection conditions) { + return where(DSL.and(conditions)); + } + + /** + * Create an inline derived table from this table + */ + @Override + public ExcerptHistory where(Condition... conditions) { + return where(DSL.and(conditions)); + } + + /** + * Create an inline derived table from this table + */ + @Override + public ExcerptHistory where(Field condition) { + return where(DSL.condition(condition)); + } + + /** + * Create an inline derived table from this table + */ + @Override + @PlainSQL + public ExcerptHistory where(SQL condition) { + return where(DSL.condition(condition)); + } + + /** + * Create an inline derived table from this table + */ + @Override + @PlainSQL + public ExcerptHistory where(@Stringly.SQL String condition) { + return where(DSL.condition(condition)); + } + + /** + * Create an inline derived table from this table + */ + @Override + @PlainSQL + public ExcerptHistory where(@Stringly.SQL String condition, Object... binds) { + return where(DSL.condition(condition, binds)); + } + + /** + * Create an inline derived table from this table + */ + @Override + @PlainSQL + public ExcerptHistory where(@Stringly.SQL String condition, QueryPart... parts) { + return where(DSL.condition(condition, parts)); + } + + /** + * Create an inline derived table from this table + */ + @Override + public ExcerptHistory whereExists(Select select) { + return where(DSL.exists(select)); + } + + /** + * Create an inline derived table from this table + */ + @Override + public ExcerptHistory whereNotExists(Select select) { + return where(DSL.notExists(select)); + } +} diff --git a/backend/src/main/java/ervu_lkrp_ul/ervu_lkrp_ul/db_beans/public_/tables/records/ExcerptHistoryRecord.java b/backend/src/main/java/ervu_lkrp_ul/ervu_lkrp_ul/db_beans/public_/tables/records/ExcerptHistoryRecord.java new file mode 100644 index 00000000..4df2d07f --- /dev/null +++ b/backend/src/main/java/ervu_lkrp_ul/ervu_lkrp_ul/db_beans/public_/tables/records/ExcerptHistoryRecord.java @@ -0,0 +1,131 @@ +/* + * This file is generated by jOOQ. + */ +package ervu_lkrp_ul.ervu_lkrp_ul.db_beans.public_.tables.records; + + +import ervu_lkrp_ul.ervu_lkrp_ul.db_beans.public_.tables.ExcerptHistory; + +import java.sql.Timestamp; + +import org.jooq.impl.TableRecordImpl; + + +/** + * This class is generated by jOOQ. + */ +@SuppressWarnings({ "all", "unchecked", "rawtypes" }) +public class ExcerptHistoryRecord extends TableRecordImpl { + + private static final long serialVersionUID = 1L; + + /** + * Setter for public.excerpt_history.id. + */ + public void setId(Long value) { + set(0, value); + } + + /** + * Getter for public.excerpt_history.id. + */ + public Long getId() { + return (Long) get(0); + } + + /** + * Setter for public.excerpt_history.ervu_id. + */ + public void setErvuId(String value) { + set(1, value); + } + + /** + * Getter for public.excerpt_history.ervu_id. + */ + public String getErvuId() { + return (String) get(1); + } + + /** + * Setter for public.excerpt_history.prn_oid. + */ + public void setPrnOid(String value) { + set(2, value); + } + + /** + * Getter for public.excerpt_history.prn_oid. + */ + public String getPrnOid() { + return (String) get(2); + } + + /** + * Setter for public.excerpt_history.url. + */ + public void setUrl(String value) { + set(3, value); + } + + /** + * Getter for public.excerpt_history.url. + */ + public String getUrl() { + return (String) get(3); + } + + /** + * Setter for public.excerpt_history.datetime. + */ + public void setDatetime(Timestamp value) { + set(4, value); + } + + /** + * Getter for public.excerpt_history.datetime. + */ + public Timestamp getDatetime() { + return (Timestamp) get(4); + } + + /** + * Setter for public.excerpt_history.status. + */ + public void setStatus(String value) { + set(5, value); + } + + /** + * Getter for public.excerpt_history.status. + */ + public String getStatus() { + return (String) get(5); + } + + // ------------------------------------------------------------------------- + // Constructors + // ------------------------------------------------------------------------- + + /** + * Create a detached ExcerptHistoryRecord + */ + public ExcerptHistoryRecord() { + super(ExcerptHistory.EXCERPT_HISTORY); + } + + /** + * Create a detached, initialised ExcerptHistoryRecord + */ + public ExcerptHistoryRecord(Long id, String ervuId, String prnOid, String url, Timestamp datetime, String status) { + super(ExcerptHistory.EXCERPT_HISTORY); + + setId(id); + setErvuId(ervuId); + setPrnOid(prnOid); + setUrl(url); + setDatetime(datetime); + setStatus(status); + resetChangedOnNotNull(); + } +} diff --git a/backend/src/main/java/ru/micord/ervu/audit/service/AuditService.java b/backend/src/main/java/ru/micord/ervu/audit/service/AuditService.java index fec9091c..dec01cc4 100644 --- a/backend/src/main/java/ru/micord/ervu/audit/service/AuditService.java +++ b/backend/src/main/java/ru/micord/ervu/audit/service/AuditService.java @@ -6,6 +6,8 @@ import ervu.model.fileupload.FileInfo; import ervu.model.fileupload.FileStatus; import ervu.model.fileupload.UploadOrgInfo; import ru.micord.ervu.audit.model.AuditActionRequest; +import ru.micord.ervu.kafka.model.ExcerptData; +import ru.micord.ervu.kafka.model.ExcerptResponse; import ru.micord.ervu.kafka.model.OrgInfo; /** @@ -21,4 +23,7 @@ public interface AuditService { void processDownloadEvent(HttpServletRequest request, long fileSize, String fileName, int formatRegistry, String status, String s3FileUrl); + + void processDownloadEvent(ExcerptData data, long fileSize, String fileName, + int formatRegistry, String status, String s3FileUrl); } diff --git a/backend/src/main/java/ru/micord/ervu/audit/service/impl/BaseAuditService.java b/backend/src/main/java/ru/micord/ervu/audit/service/impl/BaseAuditService.java index 40aad7a8..9a2a1aa0 100644 --- a/backend/src/main/java/ru/micord/ervu/audit/service/impl/BaseAuditService.java +++ b/backend/src/main/java/ru/micord/ervu/audit/service/impl/BaseAuditService.java @@ -20,6 +20,9 @@ import ru.micord.ervu.audit.model.*; import ru.micord.ervu.audit.service.AuditKafkaPublisher; import ru.micord.ervu.audit.service.AuditService; import ru.micord.ervu.exception.JsonParsingException; +import ru.micord.ervu.kafka.model.Data; +import ru.micord.ervu.kafka.model.ExcerptData; +import ru.micord.ervu.kafka.model.ExcerptResponse; import ru.micord.ervu.kafka.model.OrgInfo; import ru.micord.ervu.security.esia.model.EsiaAccessToken; import ru.micord.ervu.security.esia.service.UlDataService; @@ -123,15 +126,29 @@ public class BaseAuditService implements AuditService { } @Override - public void processDownloadEvent( - HttpServletRequest request, long fileSize, String fileName, int formatRegistry, - String status, String s3FileUrl) { + public void processDownloadEvent(HttpServletRequest request, long fileSize, String fileName, + int formatRegistry, String status, String s3FileUrl) { String userAccountId = jwtTokenService.getUserAccountId(request); + String eventTime = DateUtils.getClientDateTimeWithZone( + DateUtils.getClientDateTimeWithZoneFromRequest(request)); + publishDownloadEvent(getEsiaOrgId(request), userAccountId, eventTime, fileSize, fileName, + formatRegistry, status, s3FileUrl); + } + @Override + public void processDownloadEvent(ExcerptData data, long fileSize, String fileName, + int formatRegistry,String status, String s3FileUrl) { + String eventTime = DateUtils.getClientDateTimeWithZone(data.getTimeZone()); + publishDownloadEvent(data.getOrgId(), data.getPrnOid(), eventTime, fileSize, fileName, + formatRegistry, status, s3FileUrl); + } + + private void publishDownloadEvent(String esiaOrgId, String esiaPersonId, String eventTime, + long fileSize, String fileName, int formatRegistry, String status, String s3FileUrl) { AuditDownloadEvent event = new AuditDownloadEvent( - getEsiaOrgId(request), - userAccountId, - DateUtils.getClientDateTimeWithZoneFromRequest(request), + esiaOrgId, + esiaPersonId, + eventTime, AuditConstants.getDownloadType(formatRegistry), fileName, s3FileUrl, diff --git a/backend/src/main/java/ru/micord/ervu/audit/service/impl/StubAuditService.java b/backend/src/main/java/ru/micord/ervu/audit/service/impl/StubAuditService.java index ca82cab4..48fd927d 100644 --- a/backend/src/main/java/ru/micord/ervu/audit/service/impl/StubAuditService.java +++ b/backend/src/main/java/ru/micord/ervu/audit/service/impl/StubAuditService.java @@ -10,6 +10,7 @@ import org.springframework.stereotype.Service; import ru.micord.ervu.audit.config.AuditDisableCondition; import ru.micord.ervu.audit.model.AuditActionRequest; import ru.micord.ervu.audit.service.AuditService; +import ru.micord.ervu.kafka.model.ExcerptData; import ru.micord.ervu.kafka.model.OrgInfo; /** @@ -32,4 +33,8 @@ public class StubAuditService implements AuditService { @Override public void processDownloadEvent(HttpServletRequest request, long fileSize, String fileName, int formatRegistry, String status, String s3FileUrl) {} + + @Override + public void processDownloadEvent(ExcerptData data, long fileSize, String fileName, int formatRegistry, + String status, String s3FileUrl) {} } diff --git a/backend/src/main/java/ru/micord/ervu/controller/ExcerptController.java b/backend/src/main/java/ru/micord/ervu/controller/ExcerptController.java new file mode 100644 index 00000000..1126976c --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/controller/ExcerptController.java @@ -0,0 +1,140 @@ +package ru.micord.ervu.controller; + +import java.nio.charset.StandardCharsets; +import java.time.ZonedDateTime; +import java.util.Optional; +import java.util.TimeZone; +import java.util.UUID; + +import javax.servlet.http.HttpServletRequest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import ervu.client.fileupload.WebDavClient; +import ervu_lkrp_ul.ervu_lkrp_ul.db_beans.public_.tables.records.ExcerptHistoryRecord; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import ru.micord.ervu.audit.constants.AuditConstants; +import ru.micord.ervu.audit.service.AuditService; +import ru.micord.ervu.dao.ExcerptHistoryDao; +import ru.micord.ervu.kafka.exception.ExcerptException; +import ru.micord.ervu.kafka.model.Data; +import ru.micord.ervu.security.webbpm.jwt.UserIdsPair; +import ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil; +import ru.micord.ervu.util.UrlUtils; + +/** + * @author Eduard Tihomirov + */ +@RestController +@RequestMapping("/excerpt") +public class ExcerptController { + + @Autowired + @Qualifier("fileTemplate") + private KafkaTemplate kafkaTemplate; + + @Value("${ervu.kafka.excerpt.request.topic}") + private String requestTopic; + + @Autowired + private AuditService auditService; + + @Autowired + private WebDavClient webDavClient; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private ExcerptHistoryDao excerptHistoryDao; + + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + public void requestExcerptUrl(@RequestBody Integer year, HttpServletRequest request) { + try { + String clientTimeZone = request.getHeader("Client-Time-Zone"); + UserIdsPair userIdsPair = SecurityUtil.getUserIdsPair(); + String ervuId = userIdsPair.getErvuId(); + String prnOid = userIdsPair.getEsiaUserId(); + Data data = new Data(); + data.setErvuId(ervuId); + data.setPrnOid(prnOid); + String offset = ZonedDateTime.now(TimeZone.getTimeZone(clientTimeZone).toZoneId()) + .getOffset().getId(); + data.setTimeZone(offset); + data.setYear(year); + String messageId = UUID.randomUUID().toString(); + ProducerRecord record = new ProducerRecord<>(requestTopic, + objectMapper.writeValueAsString(data)); + record.headers().add("messageId", messageId.getBytes(StandardCharsets.UTF_8)); + kafkaTemplate.send(record).get(); + excerptHistoryDao.delete(ervuId, prnOid); + excerptHistoryDao.insert(ervuId, prnOid); + } + catch (Exception e) { + throw new ExcerptException(e); + } + } + + @GetMapping("/download") + public ResponseEntity getExcerptFile(HttpServletRequest request) { + UserIdsPair userIdsPair = SecurityUtil.getUserIdsPair(); + String ervuId = userIdsPair.getErvuId(); + String prnOid = userIdsPair.getEsiaUserId(); + String fileUrl = null; + String fileName = null; + long fileSize = 0; + + try { + Optional historyRecord = excerptHistoryDao.get(ervuId, prnOid); + + if (historyRecord.isEmpty()) { + auditService.processDownloadEvent(request, fileSize, fileName, 1, + AuditConstants.FAILURE_STATUS_TYPE, fileUrl + ); + return ResponseEntity.noContent().build(); + } + else { + fileUrl = historyRecord.get().getUrl(); + fileName = UrlUtils.extractFileNameFromUrl(fileUrl); + ResponseEntity responseEntity = webDavClient.webDavDownloadFile(fileUrl); + + if (responseEntity.getStatusCode().is2xxSuccessful()) { + fileSize = responseEntity.getHeaders().getContentLength(); + auditService.processDownloadEvent(request, fileSize, fileName, 1, + AuditConstants.SUCCESS_STATUS_TYPE, fileUrl + ); + } + else { + excerptHistoryDao.delete(ervuId, prnOid); + auditService.processDownloadEvent(request, fileSize, fileName, 1, + AuditConstants.FAILURE_STATUS_TYPE, fileUrl + ); + } + return responseEntity; + } + } + catch (Exception e) { + auditService.processDownloadEvent(request, fileSize, fileName, 1, + AuditConstants.FAILURE_STATUS_TYPE, fileUrl + ); + throw new ExcerptException(e); + } + } + + @GetMapping("/status") + public String getStatus() { + UserIdsPair userIdsPair = SecurityUtil.getUserIdsPair(); + return excerptHistoryDao.getStatus(userIdsPair.getErvuId(), userIdsPair.getEsiaUserId()); + } +} diff --git a/backend/src/main/java/ru/micord/ervu/dao/ExcerptHistoryDao.java b/backend/src/main/java/ru/micord/ervu/dao/ExcerptHistoryDao.java new file mode 100644 index 00000000..f97f5631 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/dao/ExcerptHistoryDao.java @@ -0,0 +1,97 @@ +package ru.micord.ervu.dao; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Optional; + +import ervu.enums.ExcerptStatus; +import ervu_lkrp_ul.ervu_lkrp_ul.db_beans.public_.tables.records.ExcerptHistoryRecord; +import org.jooq.DSLContext; +import org.jooq.impl.DSL; +import org.springframework.stereotype.Repository; + +import static ervu_lkrp_ul.ervu_lkrp_ul.db_beans.public_.tables.ExcerptHistory.EXCERPT_HISTORY; +import static org.jooq.impl.DSL.coalesce; +import static org.jooq.impl.DSL.field; +import static org.jooq.impl.DSL.select; + +/** + * @author gulnaz + */ +@Repository +public class ExcerptHistoryDao { + + private final DSLContext dslContext; + + public ExcerptHistoryDao(DSLContext dslContext) { + this.dslContext = dslContext; + } + + public Optional get(String ervuId, String prnOid) { + return dslContext.select(EXCERPT_HISTORY.ID, EXCERPT_HISTORY.URL) + .from(EXCERPT_HISTORY) + .where(EXCERPT_HISTORY.ID.eq( + select(DSL.max(EXCERPT_HISTORY.ID)) + .from(EXCERPT_HISTORY) + .where(EXCERPT_HISTORY.ERVU_ID.eq(ervuId) + .and(EXCERPT_HISTORY.PRN_OID.eq(prnOid))) + + )) + .fetchOptionalInto(ExcerptHistoryRecord.class); + } + + public String getStatus(String ervuId, String prnOid) { + return dslContext.select(coalesce( + field(select(EXCERPT_HISTORY.STATUS) + .from(EXCERPT_HISTORY) + .where(EXCERPT_HISTORY.ERVU_ID.eq(ervuId) + .and(EXCERPT_HISTORY.PRN_OID.eq(prnOid))) + ), ExcerptStatus.NONE.name() + )).fetchAnyInto(String.class); + } + + public void insert(String ervuId, String prnOid) { + dslContext.insertInto(EXCERPT_HISTORY) + .set(EXCERPT_HISTORY.ERVU_ID, ervuId) + .set(EXCERPT_HISTORY.PRN_OID, prnOid) + .set(EXCERPT_HISTORY.STATUS, ExcerptStatus.PENDING.name()) + .execute(); + } + + public void insert(String ervuId, String prnOid, String url, String status) { + dslContext.insertInto(EXCERPT_HISTORY) + .set(EXCERPT_HISTORY.ERVU_ID, ervuId) + .set(EXCERPT_HISTORY.PRN_OID, prnOid) + .set(EXCERPT_HISTORY.URL, url) + .set(EXCERPT_HISTORY.STATUS, status) + .execute(); + } + + public void update(Long id, String url, String status) { + dslContext.update(EXCERPT_HISTORY) + .set(EXCERPT_HISTORY.URL, url) + .set(EXCERPT_HISTORY.STATUS, status) + .set(EXCERPT_HISTORY.DATETIME, Timestamp.valueOf(LocalDateTime.now())) + .where(EXCERPT_HISTORY.ID.eq(id)) + .execute(); + } + + public void delete(String ervuId, String prnOid) { + dslContext.deleteFrom(EXCERPT_HISTORY) + .where(EXCERPT_HISTORY.ERVU_ID.eq(ervuId) + .and(EXCERPT_HISTORY.PRN_OID.eq(prnOid))) + .execute(); + } + + public void deleteOlderThan(long readyFileHours, long pendingFileHours) { + dslContext.deleteFrom(EXCERPT_HISTORY) + .where(EXCERPT_HISTORY.STATUS.eq(ExcerptStatus.READY.name()) + .and(EXCERPT_HISTORY.DATETIME.le( + Timestamp.valueOf(LocalDateTime.now().minus(readyFileHours, ChronoUnit.HOURS))))) + .or(EXCERPT_HISTORY.STATUS.in(ExcerptStatus.PENDING.name(), ExcerptStatus.NOT_FOUND.name()) + .and(EXCERPT_HISTORY.DATETIME.le( + Timestamp.valueOf(LocalDateTime.now().minus(pendingFileHours, ChronoUnit.HOURS))))) + .execute(); + } +} diff --git a/backend/src/main/java/ru/micord/ervu/kafka/ReplyingKafkaConfig.java b/backend/src/main/java/ru/micord/ervu/kafka/ReplyingKafkaConfig.java index c408bd53..d3b9783d 100644 --- a/backend/src/main/java/ru/micord/ervu/kafka/ReplyingKafkaConfig.java +++ b/backend/src/main/java/ru/micord/ervu/kafka/ReplyingKafkaConfig.java @@ -42,8 +42,6 @@ public class ReplyingKafkaConfig { private String groupId; @Value("${ervu.kafka.reply.timeout:30}") private long replyTimeout; - @Value("${ervu.kafka.excerpt.reply.topic}") - private String excerptReplyTopic; @Value("${kafka.auth_sec_proto}") private String securityProtocol; @Value("${kafka.auth_sasl_module}") @@ -119,8 +117,7 @@ public class ReplyingKafkaConfig { @Bean("ervuContainer") public ConcurrentMessageListenerContainer ervuContainer( @Qualifier("ervuContainerFactory") ConcurrentKafkaListenerContainerFactory factory) { - return factory.createContainer(orgReplyTopic, excerptReplyTopic, - journalReplyTopic, avReplyTopic); + return factory.createContainer(orgReplyTopic, journalReplyTopic, avReplyTopic); } @Bean("validateContainer") diff --git a/backend/src/main/java/ru/micord/ervu/kafka/controller/ErvuKafkaController.java b/backend/src/main/java/ru/micord/ervu/kafka/controller/ErvuKafkaController.java deleted file mode 100644 index 1488cb8f..00000000 --- a/backend/src/main/java/ru/micord/ervu/kafka/controller/ErvuKafkaController.java +++ /dev/null @@ -1,96 +0,0 @@ -package ru.micord.ervu.kafka.controller; - -import java.time.ZonedDateTime; -import java.util.TimeZone; - -import javax.servlet.http.HttpServletRequest; - -import com.fasterxml.jackson.databind.ObjectMapper; -import ervu.client.fileupload.WebDavClient; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.io.Resource; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; -import ru.micord.ervu.audit.constants.AuditConstants; -import ru.micord.ervu.audit.service.AuditService; -import ru.micord.ervu.kafka.exception.ExcerptException; -import ru.micord.ervu.kafka.exception.ExcerptResponseException; -import ru.micord.ervu.kafka.model.Data; -import ru.micord.ervu.kafka.model.ExcerptResponse; -import ru.micord.ervu.kafka.service.ReplyingKafkaService; -import ru.micord.ervu.security.webbpm.jwt.UserIdsPair; -import ru.micord.ervu.security.webbpm.jwt.util.SecurityUtil; -import ru.micord.ervu.util.UrlUtils; - -/** - * @author Eduard Tihomirov - */ -@RestController -public class ErvuKafkaController { - - @Autowired - private ReplyingKafkaService replyingKafkaService; - - @Autowired - private AuditService auditService; - - @Autowired - private WebDavClient webDavClient; - - @Value("${ervu.kafka.excerpt.reply.topic}") - private String requestReplyTopic; - - @Value("${ervu.kafka.excerpt.request.topic}") - private String requestTopic; - - @Autowired - private ObjectMapper objectMapper; - - @GetMapping(value = "/kafka/excerpt") - public ResponseEntity getExcerptFile(HttpServletRequest request) { - String fileUrl = null; - String fileName = null; - long fileSize = 0; - try { - String clientTimeZone = request.getHeader("Client-Time-Zone"); - UserIdsPair userIdsPair = SecurityUtil.getUserIdsPair(); - Data data = new Data(); - data.setErvuId(userIdsPair.getErvuId()); - data.setPrnOid(userIdsPair.getEsiaUserId()); - String offset = ZonedDateTime.now(TimeZone.getTimeZone(clientTimeZone).toZoneId()) - .getOffset().getId(); - data.setTimeZone(offset); - String kafkaResponse = replyingKafkaService.sendMessageAndGetReply(requestTopic, - requestReplyTopic, objectMapper.writeValueAsString(data)); - ExcerptResponse excerptResponse = objectMapper.readValue(kafkaResponse, ExcerptResponse.class); - - if (!excerptResponse.getSuccess()) { - throw new ExcerptResponseException( - "Error with getting excerpt url " + excerptResponse.getMessage()); - } - else if (excerptResponse.getData() == null || excerptResponse.getData().getFileUrl() == null - || excerptResponse.getData().getFileUrl().isEmpty()) { - auditService.processDownloadEvent(request, fileSize, fileName, 1, - AuditConstants.FAILURE_STATUS_TYPE, fileUrl - ); - return ResponseEntity.noContent().build(); - } - fileUrl = excerptResponse.getData().getFileUrl(); - fileName = UrlUtils.extractFileNameFromUrl(excerptResponse.getData().getFileUrl()); - ResponseEntity responseEntity = webDavClient.webDavDownloadFile(fileUrl); - fileSize = responseEntity.getHeaders().getContentLength(); - auditService.processDownloadEvent(request, fileSize, fileName, 1, - AuditConstants.SUCCESS_STATUS_TYPE, fileUrl - ); - return responseEntity; - } - catch (Exception e) { - auditService.processDownloadEvent(request, fileSize, fileName, 1, - AuditConstants.FAILURE_STATUS_TYPE, fileUrl - ); - throw new ExcerptException(e); - } - } -} diff --git a/backend/src/main/java/ru/micord/ervu/kafka/listener/ExcerptListener.java b/backend/src/main/java/ru/micord/ervu/kafka/listener/ExcerptListener.java new file mode 100644 index 00000000..c6cfb329 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/kafka/listener/ExcerptListener.java @@ -0,0 +1,94 @@ +package ru.micord.ervu.kafka.listener; + +import java.util.UUID; + +import com.fasterxml.jackson.databind.ObjectMapper; +import ervu.enums.ExcerptStatus; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Service; +import ru.micord.ervu.audit.constants.AuditConstants; +import ru.micord.ervu.audit.service.AuditService; +import ru.micord.ervu.dao.ExcerptHistoryDao; +import ru.micord.ervu.kafka.exception.ExcerptException; +import ru.micord.ervu.kafka.exception.ExcerptResponseException; +import ru.micord.ervu.kafka.model.ExcerptData; +import ru.micord.ervu.kafka.model.ExcerptResponse; +import ru.micord.ervu.util.UrlUtils; + +/** + * @author gulnaz + */ +@Service +public class ExcerptListener { + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private ExcerptHistoryDao excerptHistoryDao; + + @Autowired + private AuditService auditService; + + @Value("${ervu.kafka.group.id}") + private String groupId; + + @KafkaListener( + id = "#{excerptListener.getGroupId}", + topics = "${ervu.kafka.excerpt.reply.topic}", + containerFactory = "fileContainerFactory") + public void listen(String kafkaResponse) { + String fileUrl = null; + String fileName = null; + ExcerptData data = new ExcerptData(); + + try { + ExcerptResponse excerptResponse = objectMapper.readValue(kafkaResponse, ExcerptResponse.class); + + if (!excerptResponse.getSuccess()) { + if (excerptResponse.getData() != null) { + data = excerptResponse.getData(); + saveUrl(data.getOrgId(), data.getPrnOid(), fileUrl); + } + throw new ExcerptResponseException( + "Error with getting excerpt url " + excerptResponse.getMessage()); + } + else if (excerptResponse.getData() == null || excerptResponse.getData().getFileUrl() == null + || excerptResponse.getData().getFileUrl().isEmpty()) { + if (excerptResponse.getData() != null) { + data = excerptResponse.getData(); + saveUrl(data.getOrgId(), data.getPrnOid(), fileUrl); + } + auditService.processDownloadEvent(data, 0, fileName, 1, + AuditConstants.FAILURE_STATUS_TYPE, fileUrl + ); + return; + } + + data = excerptResponse.getData(); + fileUrl = data.getFileUrl(); + fileName = UrlUtils.extractFileNameFromUrl(fileUrl); + saveUrl(data.getOrgId(), data.getPrnOid(), fileUrl); + } + catch (Exception e) { + auditService.processDownloadEvent(data, 0, fileName, 1, + AuditConstants.FAILURE_STATUS_TYPE, fileUrl + ); + throw new ExcerptException(e); + } + } + + public String getGroupId() { + return groupId + "-" + UUID.randomUUID(); + } + + private void saveUrl(String ervuId, String prnOid, String fileUrl) { + String status = fileUrl == null ? ExcerptStatus.NOT_FOUND.name() : ExcerptStatus.READY.name(); + excerptHistoryDao.get(ervuId, prnOid).ifPresentOrElse( + record -> excerptHistoryDao.update(record.getId(), fileUrl, status), + () -> excerptHistoryDao.insert(ervuId, prnOid, fileUrl, status) + ); + } +} diff --git a/backend/src/main/java/ru/micord/ervu/kafka/model/Data.java b/backend/src/main/java/ru/micord/ervu/kafka/model/Data.java index bc2950de..8f69012e 100644 --- a/backend/src/main/java/ru/micord/ervu/kafka/model/Data.java +++ b/backend/src/main/java/ru/micord/ervu/kafka/model/Data.java @@ -16,6 +16,7 @@ public class Data implements Serializable { private String ervuId; private String prnOid; private String timeZone; + private int year; public String getErvuId() { return ervuId; @@ -40,4 +41,12 @@ public class Data implements Serializable { public void setTimeZone(String timeZone) { this.timeZone = timeZone; } + + public int getYear() { + return year; + } + + public void setYear(int year) { + this.year = year; + } } diff --git a/backend/src/main/java/ru/micord/ervu/kafka/model/ExcerptData.java b/backend/src/main/java/ru/micord/ervu/kafka/model/ExcerptData.java index abaf20f2..43f82e9f 100644 --- a/backend/src/main/java/ru/micord/ervu/kafka/model/ExcerptData.java +++ b/backend/src/main/java/ru/micord/ervu/kafka/model/ExcerptData.java @@ -11,6 +11,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; public class ExcerptData implements Serializable { private static final long serialVersionUID = 1L; private String orgId; + private String prnOid; + private String timeZone; private String fileUrl; public String getOrgId() { @@ -21,6 +23,22 @@ public class ExcerptData implements Serializable { this.orgId = orgId; } + public String getPrnOid() { + return prnOid; + } + + public void setPrnOid(String prnOid) { + this.prnOid = prnOid; + } + + public String getTimeZone() { + return timeZone; + } + + public void setTimeZone(String timeZone) { + this.timeZone = timeZone; + } + public String getFileUrl() { return fileUrl; } diff --git a/backend/src/main/java/ru/micord/ervu/scheduler/ExcerptCleanScheduler.java b/backend/src/main/java/ru/micord/ervu/scheduler/ExcerptCleanScheduler.java new file mode 100644 index 00000000..6563d146 --- /dev/null +++ b/backend/src/main/java/ru/micord/ervu/scheduler/ExcerptCleanScheduler.java @@ -0,0 +1,31 @@ +package ru.micord.ervu.scheduler; + +import net.javacrumbs.shedlock.core.SchedulerLock; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import ru.micord.ervu.dao.ExcerptHistoryDao; + +/** + * @author gulnaz + */ +@Service +public class ExcerptCleanScheduler { + + private final ExcerptHistoryDao excerptHistoryDao; + + @Value("${excerpt.ready.lifetime.hours:24}") + private long readyFileLifetime; + @Value("${excerpt.pending.lifetime.hours:8}") + private long pendingFileLifetime; + + public ExcerptCleanScheduler(ExcerptHistoryDao excerptHistoryDao) { + this.excerptHistoryDao = excerptHistoryDao; + } + + @Scheduled(cron = "${excerpt.cleanup.cron:0 */30 * * * *}") + @SchedulerLock(name = "oldExcerptFilesCleaning") + public void cleanOld() { + excerptHistoryDao.deleteOlderThan(readyFileLifetime, pendingFileLifetime); + } +} diff --git a/backend/src/main/java/ru/micord/ervu/util/DateUtils.java b/backend/src/main/java/ru/micord/ervu/util/DateUtils.java index 5147027a..9d4db21d 100644 --- a/backend/src/main/java/ru/micord/ervu/util/DateUtils.java +++ b/backend/src/main/java/ru/micord/ervu/util/DateUtils.java @@ -43,7 +43,12 @@ public final class DateUtils { public static String getClientDateTimeWithZoneFromRequest(HttpServletRequest request) { String clientTimeZone = request.getHeader("Client-Time-Zone"); + return getClientDateTimeWithZone(clientTimeZone); + } + + public static String getClientDateTimeWithZone(String clientTimeZone) { ZoneId zoneId; + try { zoneId = ZoneId.of(clientTimeZone); } diff --git a/backend/src/main/resources/config/v_1.0/2025-11-20_add-excerpt-history.xml b/backend/src/main/resources/config/v_1.0/2025-11-20_add-excerpt-history.xml new file mode 100644 index 00000000..f2f821e0 --- /dev/null +++ b/backend/src/main/resources/config/v_1.0/2025-11-20_add-excerpt-history.xml @@ -0,0 +1,20 @@ + + + + + Add excerpt history table + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/config/v_1.0/2025-11-26_add-status-column-to-excerpt-history.xml b/backend/src/main/resources/config/v_1.0/2025-11-26_add-status-column-to-excerpt-history.xml new file mode 100644 index 00000000..91b6a51d --- /dev/null +++ b/backend/src/main/resources/config/v_1.0/2025-11-26_add-status-column-to-excerpt-history.xml @@ -0,0 +1,21 @@ + + + + + + + + + + Add status column to excerpt history table + + + + + + + + + + diff --git a/backend/src/main/resources/config/v_1.0/changelog-v_1.0.xml b/backend/src/main/resources/config/v_1.0/changelog-v_1.0.xml index 4180dff3..92791bd2 100644 --- a/backend/src/main/resources/config/v_1.0/changelog-v_1.0.xml +++ b/backend/src/main/resources/config/v_1.0/changelog-v_1.0.xml @@ -11,5 +11,7 @@ + + - \ No newline at end of file + diff --git a/config/micord.env b/config/micord.env index 3664f24a..7c608260 100644 --- a/config/micord.env +++ b/config/micord.env @@ -70,7 +70,10 @@ WEBDAV_URLS=https://ervu-webdav.k8s.micord.ru,https://ervu-webdav1.k8s.micord.ru WEBDAV_USERNAME=test WEBDAV_PASSWORD=test WEBDAV_BAD_SERVERS_CACHE_EXPIRE_SECONDS=120 -WEBDAV_CLEANUP_CRON=0 0 0 * * * +WEBDAV_CLEANUP_CRON=0 */30 * * * * WEBDAV_RETRY_DELAY=500 -FILE_WEBDAV_LIFETIME_SECONDS=300 +FILE_WEBDAV_LIFETIME_SECONDS=86400 FILE_WEBDAV_EXTENSIONS=csv,xlsx +EXCERPT_CLEANUP_CRON=0 */30 * * * * +EXCERPT_READY_LIFETIME_HOURS=24 +EXCERPT_PENDING_LIFETIME_HOURS=8 diff --git a/ervu_lkrp_ul-openapi.yaml b/ervu_lkrp_ul-openapi.yaml index 51c932b3..981e73c7 100644 --- a/ervu_lkrp_ul-openapi.yaml +++ b/ervu_lkrp_ul-openapi.yaml @@ -108,11 +108,43 @@ paths: application/json: schema: type: string - /kafka/excerpt: + /excerpt: + post: + summary: Отправка запроса на формирование выписки + operationId: requestExcerptUrl + description: Отправка запроса на формирование выписки по журналу взаимодействий с ЕРВУ + parameters: + - name: Client-Time-Zone + in: header + required: true + description: Таймзона клиента + schema: + type: string + requestBody: + content: + application/json: + schema: + required: + - year + type: integer + properties: + year: + description: Период формирования выписки + responses: + "200": + description: OK + /excerpt/download: get: summary: Получение выписки operationId: getExcerptFile description: Получение выписки по журналу взаимодействий с ЕРВУ + parameters: + - name: Client-Time-Zone + in: header + required: true + description: Таймзона клиента + schema: + type: string responses: "200": description: OK @@ -127,6 +159,18 @@ paths: type: object "204": description: No Content + /excerpt/status: + get: + summary: Получение статуса выписки + operationId: getStatus + description: Получение статуса выписки по журналу взаимодействий с ЕРВУ (NONE, PENDING, READY, NOT_FOUND) + responses: + "200": + description: OK + content: + application/json: + schema: + type: string /rpc/filesentlog/bbaf33d7-0679-440b-a394-cb805ce80300/ru.micord.ervu.service.rpc.InMemoryStaticGridRpcService/loadData: post: summary: Получение данных по журналу взаимодействий diff --git a/frontend/src/resources/css/components-lkrp.css b/frontend/src/resources/css/components-lkrp.css index e31c124d..22efadaf 100644 --- a/frontend/src/resources/css/components-lkrp.css +++ b/frontend/src/resources/css/components-lkrp.css @@ -119,10 +119,24 @@ .webbpm.ervu_lkrp_ul ervu-download-file-button { display: block; } -.webbpm.ervu_lkrp_ul .fieldset text + ervu-download-file-button { +.webbpm.ervu_lkrp_ul .fieldset ervu-download-file-button, +.webbpm.ervu_lkrp_ul year-combo-box .selectize-input { margin-top: var(--indent-mini); } +.webbpm.ervu_lkrp_ul year-combo-box .selectize-dropdown-content { + padding: 5px 0; +} + +.webbpm.ervu_lkrp_ul .message-clock { + background: url(../img/svg/clock-32x32.svg) no-repeat 0 0; + display: block; + font-size: var(--l-size-text-primary); + height: 32px; + margin: var(--indent-mini) 0; + padding-left: var(--indent-big); +} + .webbpm.ervu_lkrp_ul .btn, .webbpm.ervu_lkrp_ul .fieldset .btn-main:not(.info):not(.link) .btn, .webbpm.ervu_lkrp_ul .modal.show button-component.btn-main:not(.info):not(.link) .btn, @@ -994,4 +1008,4 @@ } .webbpm.ervu_lkrp_ul #mydata .right-block field-set:first-child .fieldset::before { display: none; -} \ No newline at end of file +} diff --git a/frontend/src/resources/template/ervu/component/combobox/ComboBox.html b/frontend/src/resources/template/ervu/component/combobox/ComboBox.html new file mode 100644 index 00000000..48ffb53f --- /dev/null +++ b/frontend/src/resources/template/ervu/component/combobox/ComboBox.html @@ -0,0 +1,45 @@ +
+ +
+ + +
+
+
+
+ {{warning}} +
+
+
+
+ {{customValidationMessage}} +
+
+
+ {{getErrorMessages()['required']}} +
+
+ +
+
+
+
+ {{getVersioningValidationMessage()}} +
+
+
+
+
diff --git a/frontend/src/ts/ervu/component/button/ErvuDownloadFileButton.ts b/frontend/src/ts/ervu/component/button/ErvuDownloadFileButton.ts index 80c65d64..6b50c255 100644 --- a/frontend/src/ts/ervu/component/button/ErvuDownloadFileButton.ts +++ b/frontend/src/ts/ervu/component/button/ErvuDownloadFileButton.ts @@ -1,8 +1,6 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef} from "@angular/core"; import {AbstractButton, MessagesService, NotNull, ObjectRef} from "@webbpm/base-package"; import {HttpClient, HttpHeaders} from "@angular/common/http"; -import {InMemoryStaticGrid} from "../grid/InMemoryStaticGrid"; -import {Subscription} from "rxjs"; /** * @author: Eduard Tihomirov @@ -14,13 +12,9 @@ import {Subscription} from "rxjs"; changeDetection: ChangeDetectionStrategy.OnPush }) export class ErvuDownloadFileButton extends AbstractButton { + private httpClient: HttpClient; private messageService: MessagesService; - private gridLoadedSubscription: Subscription; - - @ObjectRef() - @NotNull() - public grid: InMemoryStaticGrid; @NotNull() public fileName: string; @@ -36,29 +30,10 @@ export class ErvuDownloadFileButton extends AbstractButton { super.initialize(); this.httpClient = this.injector.get(HttpClient); this.messageService = this.injector.get(MessagesService); - if (this.grid.isInitialized() && this.grid.getRowDataSize() > 0) { - this.setEnabled(true); - this.setVisible(true); - } - this.gridLoadedSubscription = this.grid.gridLoaded.subscribe(() => { - if (this.grid.getRowDataSize() > 0) { - this.setEnabled(true); - this.setVisible(true); - } - else { - this.setEnabled(false); - this.setVisible(false); - } - }); - } - - ngOnDestroy() { - super.ngOnDestroy(); - this.gridLoadedSubscription.unsubscribe(); } public doClickActions(): Promise { - return this.httpClient.get('kafka/excerpt', { + return this.httpClient.get('excerpt/download', { responseType: 'blob', headers: new HttpHeaders({'Client-Time-Zone' : Intl.DateTimeFormat().resolvedOptions().timeZone}), observe: 'response' diff --git a/frontend/src/ts/ervu/component/button/ErvuFileRequestButton.ts b/frontend/src/ts/ervu/component/button/ErvuFileRequestButton.ts new file mode 100644 index 00000000..19b75deb --- /dev/null +++ b/frontend/src/ts/ervu/component/button/ErvuFileRequestButton.ts @@ -0,0 +1,67 @@ +import { + AbstractButton, AnalyticalScope, + Behavior, + ComboBox, Dialog, + MessagesService, + NotNull, + ObjectRef +} from "@webbpm/base-package"; +import {HttpClient, HttpHeaders} from "@angular/common/http"; + +@AnalyticalScope(AbstractButton) +export class ErvuFileRequestButton extends Behavior { + + @NotNull() + @ObjectRef() + public year: ComboBox; + + @NotNull() + @ObjectRef() + public beforeRequestDialog: Dialog; + + @NotNull() + @ObjectRef() + public afterRequestDialog: Dialog; + + private button: AbstractButton; + private httpClient: HttpClient; + private messageService: MessagesService; + private onClickFunction: Function; + private onYearValueChange: Function; + + initialize() { + super.initialize(); + this.button = this.getScript(AbstractButton); + this.httpClient = this.injector.get(HttpClient); + this.messageService = this.injector.get(MessagesService); + this.onClickFunction = () => { + if (!this.year.getValue()) { + this.messageService.error("Не выбран период"); + return; + } + + this.httpClient.post("excerpt", this.year.getValue(), { + headers: new HttpHeaders( + {'Client-Time-Zone': Intl.DateTimeFormat().resolvedOptions().timeZone}) + }).toPromise() + .then(() => { + this.beforeRequestDialog.hide(); + this.afterRequestDialog.show(); + }) + .catch(e => this.messageService.error(e)); + }; + this.onYearValueChange = () => this.button.setEnabled(this.year.getValue()); + } + + bindEvents() { + super.bindEvents(); + this.button.addClickListener(this.onClickFunction); + this.year.addUserChangeValueListener(this.onYearValueChange); + } + + unbindEvents() { + super.unbindEvents(); + this.button.removeClickListener(this.onClickFunction); + this.year.removeUserChangeValueListener(this.onYearValueChange); + } +} diff --git a/frontend/src/ts/ervu/component/button/ErvuFileRequestDialogButton.ts b/frontend/src/ts/ervu/component/button/ErvuFileRequestDialogButton.ts new file mode 100644 index 00000000..ba515547 --- /dev/null +++ b/frontend/src/ts/ervu/component/button/ErvuFileRequestDialogButton.ts @@ -0,0 +1,76 @@ +import {AbstractButton, AnalyticalScope, Behavior, NotNull, ObjectRef, Text} from "@webbpm/base-package"; +import {Subscription} from "rxjs"; +import {InMemoryStaticGrid} from "../grid/InMemoryStaticGrid"; +import {OnDestroy} from "@angular/core"; +import {HttpClient} from "@angular/common/http"; +import { ExcerptStatus } from "../../../generated/ervu/enums/ExcerptStatus"; + +@AnalyticalScope(AbstractButton) +export class ErvuFileRequestDialogButton extends Behavior implements OnDestroy { + + @ObjectRef() + @NotNull() + public grid: InMemoryStaticGrid; + + @ObjectRef() + @NotNull() + public text: Text; + + @ObjectRef() + @NotNull() + public downloadButton: AbstractButton; + + @ObjectRef() + @NotNull() + public notFoundText: Text; + + @NotNull() + public requestSentMsg: string; + + private button: AbstractButton; + private httpClient: HttpClient; + private gridLoadedSubscription: Subscription; + private fileStatus: string = ExcerptStatus.NONE; + + initialize() { + super.initialize(); + this.button = this.getScript(AbstractButton); + this.httpClient = this.injector.get(HttpClient); + this.gridLoadedSubscription = this.grid.gridLoaded.subscribe(() => { + this.button.setEnabled(this.grid.getRowDataSize() > 0 + && this.fileStatus !== ExcerptStatus.PENDING); + this.button.setVisible(this.grid.getRowDataSize() > 0); + }); + } + + start() { + super.start(); + this.getFileStatus() + .then(status => { + this.fileStatus = status; + + if (this.grid.isInitialized() && this.grid.getRowDataSize() > 0) { + this.button.setEnabled(status !== ExcerptStatus.PENDING); + this.button.setVisible(true); + } + + if (status === ExcerptStatus.PENDING) { + this.button.setTooltip(this.requestSentMsg); + this.text.setValue(this.requestSentMsg); + + } + this.downloadButton.setEnabled(status === ExcerptStatus.READY); + this.downloadButton.setVisible(status === ExcerptStatus.READY); + this.notFoundText.setVisible(status === ExcerptStatus.NOT_FOUND); + }); + } + + private getFileStatus(): Promise { + return this.httpClient.get('excerpt/status', {observe: 'response'}).toPromise() + .then(res => res.body.toString()); + } + + ngOnDestroy() { + this.gridLoadedSubscription.unsubscribe(); + } +} diff --git a/frontend/src/ts/ervu/component/combobox/YearComboBox.ts b/frontend/src/ts/ervu/component/combobox/YearComboBox.ts new file mode 100644 index 00000000..3e61e1db --- /dev/null +++ b/frontend/src/ts/ervu/component/combobox/YearComboBox.ts @@ -0,0 +1,36 @@ +import {ComboBox, ComboBoxModel} from "@webbpm/base-package"; +import {ChangeDetectionStrategy, Component} from "@angular/core"; + +@Component({ + moduleId: module.id, + selector: 'year-combo-box', + templateUrl: './../../../../../src/resources/template/ervu/component/combobox/ComboBox.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class YearComboBox extends ComboBox { + + private START_YEAR: number = 2025; + + initialize(): Promise { + return this.loadData(); + } + + loadData(): Promise { + let currentYear = new Date().getFullYear(); + let startYear = this.START_YEAR; + let years: ComboBoxModel[] = []; + + while (startYear <= currentYear) { + let value = startYear++; + let model: ComboBoxModel = new ComboBoxModel(); + model.id = value; + model.label = `${value}`; + model.dropDownLabel = `${value}`; + model.valid = true; + model.hidden = false; + years.push(model); + } + return Promise.resolve() + .then(() => this.setData(years)); + } +} diff --git a/frontend/src/ts/modules/app/app.module.ts b/frontend/src/ts/modules/app/app.module.ts index 00f1ad4d..0a8732fd 100644 --- a/frontend/src/ts/modules/app/app.module.ts +++ b/frontend/src/ts/modules/app/app.module.ts @@ -30,6 +30,7 @@ import { ErvuFileUploadWithAdditionalFiles } from "../../ervu/component/fileupload/ErvuFileUploadWithAdditionalFiles"; import {ValidateFileService} from "../../ervu/service/ValidateFileService"; +import {YearComboBox} from "../../ervu/component/combobox/YearComboBox"; registerLocaleData(localeRu); export const DIRECTIVES = [ @@ -42,6 +43,7 @@ export const DIRECTIVES = [ forwardRef(() => ErvuFileUploadWithAdditionalFiles), forwardRef(() => ErvuDownloadFileButton), forwardRef(() => InMemoryStaticGrid), + forwardRef(() => YearComboBox) ]; export function checkAuthentication(authService: AuthenticationService): () => Promise { diff --git a/resources/src/main/resources/business-model/Журнал взаимодействия.page b/resources/src/main/resources/business-model/Журнал взаимодействия.page index cebeb9fd..f7278a27 100644 --- a/resources/src/main/resources/business-model/Журнал взаимодействия.page +++ b/resources/src/main/resources/business-model/Журнал взаимодействия.page @@ -2037,13 +2037,6 @@ - - d7d54cfb-26b5-4dba-b56f-b6247183c24d - 358d1918-2e9b-4123-a8b5-8189a1789291 - Горизонтальный контейнер - true - true - ba24d307-0b91-4299-ba82-9d0b52384ff2 097a2141-13d0-4b15-ac5f-5c310b3753f7 @@ -2066,7 +2059,7 @@ initialValue -"Запросите выписку, чтобы посмотреть журнал взаимодействий за весь период" +"Запросите выписку о переданных сведениях по сотрудникам в Военные комиссариаты" @@ -2084,67 +2077,489 @@ false + + d7d54cfb-26b5-4dba-b56f-b6247183c24d + a3c68e2f-424c-4908-9983-b5e3adf8805b + Hbox + true + false + + + + + + + fd7e47b9-dce1-4d14-9f3a-580c79f59579 + cddb82f9-6e93-4fde-a30c-60bbced3b380 + Кнопка диалога запроса выписки + false + false + + + +caption + + "Запросить выписку" + + + +disabled + + true + + + +visible + + false + + + + + + + ErvuFileRequestDialogButton + ervu.component.button + + true + true + + +downloadButton + + {"objectId":"2528ea8a-3e3f-4968-a973-8e9ac1ca4326","packageName":"ervu.component.button","className":"ErvuDownloadFileButton","type":"TS"} + + + +grid + + {"objectId":"bbaf33d7-0679-440b-a394-cb805ce80300","packageName":"ervu.component.grid","className":"InMemoryStaticGrid","type":"TS"} + + + +notFoundText + + {"objectId":"06657c0b-4e2e-402b-a40b-099d15da551d","packageName":"component","className":"Text","type":"TS"} + + + +requestSentMsg + + "Дождитесь формирования выписки" + + + +text + + {"objectId":"097a2141-13d0-4b15-ac5f-5c310b3753f7","packageName":"component","className":"Text","type":"TS"} + + + + + + + fd7e47b9-dce1-4d14-9f3a-580c79f59579 + 2528ea8a-3e3f-4968-a973-8e9ac1ca4326 + Скачать + false + false + + false + true + + + + ErvuDownloadFileButton + ervu.component.button + + true + true + + +caption + + "Скачать выписку" + + + +disabled + + true + + + +fileName + + "Выписка.xlsx" + + + +noFileMessage + + "Нет записей в выписке журнала взаимодействия" + + + +visible + + false + + + + + + + ba24d307-0b91-4299-ba82-9d0b52384ff2 + 06657c0b-4e2e-402b-a40b-099d15da551d + Выписка не найдена + false + false + + + +collectible + + false + + + +initialValue + + "Выписка по предыдущему запросу не найдена" + + + +style + + + + margin + + "16px" + + + + padding + + "12px 0 0" + + + + + + +visible + + false + + + + + + + + + false + + + + 86f297f1-ab3d-40e0-ac2f-89cc944b7f0a + cb657c69-1023-4462-ada6-03c1a9fbcb8e + Dialog: запросить выписку + true + false + + + + + + + b310f98a-69c6-4e7b-8cdb-f1ab9f9c0d94 + 55a6bd55-8a44-4b4f-8070-9dc36d2bcf3a + Выберите период + false + false + + false + + + collectible + + false + + + + + + false + false + + + false + false + + + false + false + + + false + false + + + false + false + + + +YearComboBox +ervu.component.combobox + + true + true + + + label + + "Выберите период" + + + + visible + + true + + + + + + + fd7e47b9-dce1-4d14-9f3a-580c79f59579 + 6a2c076b-7a39-4afc-bb43-49fff00b21e3 + Запросить выписку + false + false + + + + caption + + "Запросить выписку" + + + + disabled + + true + + + + + + +ErvuFileRequestButton +ervu.component.button + + true + true + + + afterRequestDialog + + {"objectId":"6ed640f7-eab1-4dcc-99c0-c9a49c85292f","packageName":"component","className":"Dialog","type":"TS"} + + + + beforeRequestDialog + + {"objectId":"cb657c69-1023-4462-ada6-03c1a9fbcb8e","packageName":"component","className":"Dialog","type":"TS"} + + + + year + + {"objectId":"55a6bd55-8a44-4b4f-8070-9dc36d2bcf3a","packageName":"component.field","className":"ComboBox","type":"TS"} + + + + + + + + 86f297f1-ab3d-40e0-ac2f-89cc944b7f0a + 6ed640f7-eab1-4dcc-99c0-c9a49c85292f + Dialog: запрос отправлен + true + false + + + + + + + ba24d307-0b91-4299-ba82-9d0b52384ff2 + ead2d930-7157-4d98-b778-67f6a378bcd7 + Заголовок + false + false + + + + collectible + + false + + + + cssClasses + + + + "title" + + + + + + initialValue + + "Запрос отправлен" + + + + + + + + + false + + + + ba24d307-0b91-4299-ba82-9d0b52384ff2 + 882c5346-4c05-43eb-9675-8fc860565429 + Сообщение + false + false + + + + collectible + + false + + + + initialValue + + "Выписка будет сформирована Государственной информационной системой \"Единый реестр воинского учета\" (ГИС ЕРВУ) и направлена в ваш личный кабинет Реестра повесток" + + + + + + + + + false + + + + ba24d307-0b91-4299-ba82-9d0b52384ff2 + b84e5d1f-ad34-44b9-aca4-29b2083aea37 + Время + false + false + + + + collectible + + false + + + + cssClasses + + + + "message-clock" + + + + + + initialValue + + "Услуга будет оказана в течение дня" + + + + + + + + + false + + + + c8dfe691-a84a-48da-b79e-6298d90db71d + b8655a1c-b30f-426c-bbd4-5799281f0752 + Navigation: В личный кабинет + false + false + + + + caption + + "В личный кабинет" + + + + navigateTo + + "/" + + + + + + + + + 86f297f1-ab3d-40e0-ac2f-89cc944b7f0a + 6ed640f7-eab1-4dcc-99c0-c9a49c85292f + Dialog: запрос отправлен + true + true + + + fd7e47b9-dce1-4d14-9f3a-580c79f59579 + cddb82f9-6e93-4fde-a30c-60bbced3b380 + Кнопка диалога запроса выписки + false + true + + + 86f297f1-ab3d-40e0-ac2f-89cc944b7f0a + cb657c69-1023-4462-ada6-03c1a9fbcb8e + Dialog + true + true + fd7e47b9-dce1-4d14-9f3a-580c79f59579 2528ea8a-3e3f-4968-a973-8e9ac1ca4326 - Кнопка + Скачать false - false - - false - true - - - - ErvuDownloadFileButton - ervu.component.button - - true - true - - - caption - -"Запросить выписку" - - - - disabled - -true - - - - fileName - -"Выписка.xlsx" - - - - grid - -{"objectId":"bbaf33d7-0679-440b-a394-cb805ce80300","packageName":"ervu.component.grid","className":"InMemoryStaticGrid","type":"TS"} - - - - noFileMessage - -"Нет записей в выписке журнала взаимодействия" - - - - visible - -false - - - - + true - + 98594cec-0a9b-4cef-af09-e1b71cb2ad9e - 78640b5d-37d1-4940-aafc-fc7a38b907cf - Action Controller + f4ae195f-3afe-473e-ba9e-72c966b55b25 + AС: видимость текста false false @@ -2152,7 +2567,9 @@ elseActions - + + + @@ -2192,13 +2609,13 @@ eventRefs - + behavior - {"objectId":"2528ea8a-3e3f-4968-a973-8e9ac1ca4326","packageName":"ervu.component.button","className":"ErvuDownloadFileButton","type":"TS"} + {"objectId":"cddb82f9-6e93-4fde-a30c-60bbced3b380","packageName":"component.button","className":"Button","type":"TS"} @@ -2219,7 +2636,7 @@ conditions - + @@ -2243,7 +2660,7 @@ behavior - {"objectId":"2528ea8a-3e3f-4968-a973-8e9ac1ca4326","packageName":"ervu.component.button","className":"ErvuDownloadFileButton","type":"TS"} + {"objectId":"cddb82f9-6e93-4fde-a30c-60bbced3b380","packageName":"component.button","className":"Button","type":"TS"} @@ -2301,7 +2718,9 @@ thenActions - + + + @@ -2341,6 +2760,330 @@ + + b310f98a-69c6-4e7b-8cdb-f1ab9f9c0d94 + 55a6bd55-8a44-4b4f-8070-9dc36d2bcf3a + Выберите период + false + true + + + fd7e47b9-dce1-4d14-9f3a-580c79f59579 + 6a2c076b-7a39-4afc-bb43-49fff00b21e3 + Запросить выписку + false + true + + + 98594cec-0a9b-4cef-af09-e1b71cb2ad9e + 7fbfc96c-0c1a-46e0-938b-ec45e75341bd + AC: показать диалог для запроса + false + false + + + + eventRefs + + + + + + behavior + + {"objectId":"cddb82f9-6e93-4fde-a30c-60bbced3b380","packageName":"component.button","className":"Button","type":"TS"} + + + + propertyName + + "clickEvent" + + + + + + + + + ifCondition + + + + logicalOperation + + null + + + + + + + thenActions + + + + + + behavior + + {"objectId":"cb657c69-1023-4462-ada6-03c1a9fbcb8e","packageName":"component","className":"Dialog","type":"TS"} + + + + method + + "show" + + + + value + + null + + + + + + + + + + + + 98594cec-0a9b-4cef-af09-e1b71cb2ad9e + 8825f9d1-53b1-438f-9b32-4850eba28bd7 + AC: доступность кнопки + false + false + + + + eventRefs + + + + + + behavior + + {"objectId":"6ed640f7-eab1-4dcc-99c0-c9a49c85292f","packageName":"component","className":"Dialog","type":"TS"} + + + + propertyName + + "hideDialogEvent" + + + + + + + + + ifCondition + + + + logicalOperation + + null + + + + + + + thenActions + + + + + + behavior + + {"objectId":"cddb82f9-6e93-4fde-a30c-60bbced3b380","packageName":"component.button","className":"Button","type":"TS"} + + + + method + + "setEnabled" + + + + value + + + + staticValue + + + boolean + + +false + + + + + + + + + + + + + behavior + + {"objectId":"cddb82f9-6e93-4fde-a30c-60bbced3b380","packageName":"component.button","className":"Button","type":"TS"} + + + + method + + "setTooltip" + + + + value + + + + staticValue + + + string + + +"Дождитесь формирования выписки" + + + + + + + + + + + + + behavior + + {"objectId":"097a2141-13d0-4b15-ac5f-5c310b3753f7","packageName":"component","className":"Text","type":"TS"} + + + + method + + "setValue" + + + + value + + + + staticValue + + + string + + +"Дождитесь формирования выписки" + + + + + + + + + + + + + behavior + + {"objectId":"2528ea8a-3e3f-4968-a973-8e9ac1ca4326","packageName":"ervu.component.button","className":"ErvuDownloadFileButton","type":"TS"} + + + + method + + "setEnabled" + + + + value + + + + staticValue + + + boolean + + +false + + + + + + + + + + + + + behavior + + {"objectId":"2528ea8a-3e3f-4968-a973-8e9ac1ca4326","packageName":"ervu.component.button","className":"ErvuDownloadFileButton","type":"TS"} + + + + method + + "setVisible" + + + + value + + + + staticValue + + + boolean + + +false + + + + + + + + + + + + + + + f4ca0e36-67d2-4882-9032-96718d6892da + 0480ebf1-7636-46f4-9b68-7a30c08b7d4d + Download button + false + true + 3057d447-6d17-48a8-b096-b14ea88d17e8 f4d7dfb7-67e6-41bd-bef6-ae9751ce2088 @@ -2348,13 +3091,6 @@ false true - - d7d54cfb-26b5-4dba-b56f-b6247183c24d - 484748b4-a03d-4254-a7f3-fec2402e7866 - Горизонтальный контейнер - true - true - e32ae1f5-5b14-45f1-abb6-f52c34b3b570 87481ea6-30aa-4002-81d0-421e066298ff @@ -2384,5 +3120,12 @@ true + + a4d442cf-72a8-431e-815a-490959083787 + 9078f42f-c838-4d38-9e46-4419d12eb80d + SecurityFeatures + true + true +