/
evg299
/
selsup-task
Обзор
Документация
Войти
/
evg299
/
selsup-task
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/main/java/selsup/task/CrptApi.java
306 строк
10 KB
evgeny
init
28 окт 2025, 15:33
28 окт 2025, 15:33
349072a
Код
Авторство
О чём код?
package selsup.task; import com.fasterxml.jackson.annotation.JsonProperty; import io.github.resilience4j.ratelimiter.RateLimiter; import io.github.resilience4j.ratelimiter.RateLimiterConfig; import io.github.resilience4j.ratelimiter.RequestNotPermitted; import lombok.*; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.entity.StringEntity; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.LocalDate; import java.util.Base64; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; public class CrptApi implements AutoCloseable { public static final String DOMAIN = "https://ismp.crpt.ru"; public static final String LIMITER_NAME = "crpt-limiter"; public static final int MAX_CONNECTIONS = 100; // тут надо поэкспериментировать private final RateLimiter rateLimiter; private final CloseableHttpClient httpClient; private final ObjectMapper mapper; public CrptApi(TimeUnit timeUnit, int requestLimit) { this.rateLimiter = RateLimiter.of(LIMITER_NAME, RateLimiterConfig.custom() .limitForPeriod(requestLimit) .limitRefreshPeriod(Duration.ofNanos(timeUnit.toNanos(1))) .timeoutDuration(Duration.ZERO) .build()); PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); cm.setMaxTotal(MAX_CONNECTIONS); this.httpClient = HttpClients.custom() .setConnectionManager(cm) .build(); this.mapper = JsonMapper.builder() .findAndAddModules() .build(); } private <T> T executeWithRateLimit(Supplier<T> supplier) { Supplier<T> limited = RateLimiter.decorateSupplier(rateLimiter, supplier); try { return limited.get(); } catch (RequestNotPermitted ex) { throw new RateLimitExceededException(); } } /** * Запрос авторизаций * * @return */ public AuthData authStep1() { return executeWithRateLimit(() -> { try { return httpClient.execute(new HttpGet(DOMAIN + "/api/v3/auth/cert/"), response -> mapper.readValue(EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8), AuthData.class)); } catch (IOException e) { throw new RuntimeException(e); } }); } /** * Получение аутентификационного токена * * @param uuid * @param signature * @return */ public Token authStep2(String uuid, String signature) { return executeWithRateLimit(() -> { try { HttpPost post = new HttpPost(DOMAIN + "/api/v3/auth/cert/"); post.setHeader("Content-Type", "application/json"); post.setEntity(new StringEntity(mapper.writeValueAsString(new AuthData(uuid, signature)), ContentType.APPLICATION_JSON)); return httpClient.execute(post, response -> mapper.readValue(EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8), Token.class)); } catch (IOException e) { throw new RuntimeException(e); } }); } /** * Ввод в оборот товара, произведенного на территории РФ * * @param document * @param signature * @param productGroup * @param authToken - то, что получаем в {@link selsup.task.CrptApi#authStep2(java.lang.String, java.lang.String) } * @return */ public CreateDocResponse introduceGoods(IntroduceGoodsDocument document, String signature, ProductGroup productGroup, String authToken) { return executeWithRateLimit(() -> { // String docJson = mapper.writeValueAsString(document); String docB64 = Base64.getEncoder().encodeToString(docJson.getBytes(StandardCharsets.UTF_8)); // HttpPost post = new HttpPost(DOMAIN + "/api/v3/lk/documents/create?pg=" + productGroup.getKey()); post.setHeader("Content-Type", "application/json"); post.setHeader("Authorization", "Bearer " + authToken); CreateDocBody body = CreateDocBody.builder() .documentFormat(DocumentFormat.MANUAL) .type("LP_INTRODUCE_GOODS") .signature(signature) .productDocument(docB64) .build(); post.setEntity(new StringEntity(mapper.writeValueAsString(body), ContentType.APPLICATION_JSON)); // try { return httpClient.execute(post, response -> mapper.readValue(EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8), CreateDocResponse.class) ); } catch (IOException e) { throw new RuntimeException(e); } }); } @Override public void close() throws Exception { if (null != httpClient) httpClient.close(); } } /// ////////////////////////////////////////////// @Data @NoArgsConstructor @AllArgsConstructor class AuthData { private String uuid; private String data; } @Data class Token { private String token; } enum DocumentFormat { MANUAL, XML, CSV } @Getter @AllArgsConstructor enum ProductGroup { CLOTHES(1, "clothes", "Предметы одежды, белье постельное, столовое, туалетное и кухонное"), SHOES(2, "shoes", "Обувные товары"), TOBACCO(3, "tobacco", "Табачная продукция"), PERFUMERY(4, "perfumery", "Духи и туалетная вода"), TIRES(5, "tires", "Шины и покрышки пневматические резиновые новые"), ELECTRONICS(6, "electronics", "Фотокамеры (кроме кинокамер), фотовспышки и лампы-вспышки"), PHARMA(7, "pharma", "Лекарственные препараты для медицинского применения"), MILK(8, "milk", "Молочная продукция"), BICYCLE(9, "bicycle", "Велосипеды и велосипедные рамы"), WHEELCHAIRS(10, "wheelchairs", "Кресла-коляски"); private final int code; private final String key; private final String description; public static ProductGroup fromCode(int code) { for (ProductGroup type : values()) { if (type.code == code) { return type; } } throw new IllegalArgumentException("Unknown code: " + code); } public static ProductGroup fromKey(String key) { for (ProductGroup type : values()) { if (type.key.equalsIgnoreCase(key)) { return type; } } throw new IllegalArgumentException("Unknown key: " + key); } } @Data @Builder class CreateDocResponse { private String value; private String code; private String error_message; private String description; } @Data @Builder class CreateDocBody { private String productDocument; private DocumentFormat documentFormat; private String type; private String signature; } @Data class IntroduceGoodsDocument { private Description description; @JsonProperty("doc_id") private String docId; @JsonProperty("doc_status") private String docStatus; @JsonProperty("doc_type") private String docType; @JsonProperty("importRequest") private boolean importRequest; @JsonProperty("owner_inn") private String ownerInn; @JsonProperty("participant_inn") private String participantInn; @JsonProperty("producer_inn") private String producerInn; @JsonProperty("production_date") private LocalDate productionDate; @JsonProperty("production_type") private String productionType; private List<Product> products; @JsonProperty("reg_date") private LocalDate regDate; @JsonProperty("reg_number") private String regNumber; } @Data class Description { @JsonProperty("participantInn") private String participantInn; } @Data class Product { @JsonProperty("certificate_document") private String certificateDocument; @JsonProperty("certificate_document_date") private LocalDate certificateDocumentDate; @JsonProperty("certificate_document_number") private String certificateDocumentNumber; @JsonProperty("owner_inn") private String ownerInn; @JsonProperty("producer_inn") private String producerInn; @JsonProperty("production_date") private LocalDate productionDate; @JsonProperty("tnved_code") private String tnvedCode; @JsonProperty("uit_code") private String uitCode; @JsonProperty("uitu_code") private String uituCode; } class RateLimitExceededException extends RuntimeException { public RateLimitExceededException() { super("Rate limit exceeded"); } }