94 Commits
Author SHA1 Message Date
backdev 4dd6c4a3c0 dev merge
Test Workflow / test (push) Has been cancelled
2026-04-22 16:02:52 +07:00
backdev da84490198 dev add new action
Test Workflow / test (push) Has been cancelled
2026-04-22 14:23:35 +07:00
backdev b723247b9c dev add new action
Test Workflow / test (push) Has been cancelled
2026-04-22 14:04:57 +07:00
backdev cc43b92f69 dev test baseurl for test
Test Workflow / test (push) Has been cancelled
2026-04-22 02:06:39 +07:00
backdev d92a9a7d86 dev test baseurl for test
Test Workflow / test (push) Has been cancelled
2026-04-22 01:50:18 +07:00
backdev 6dec7838ce dev test baseurl for test
Test Workflow / test (push) Has been cancelled
2026-04-22 01:06:19 +07:00
backdev 83ed7c5b01 dev test baseurl for test
Test Workflow / test (push) Has been cancelled
2026-04-21 21:11:47 +07:00
backdev e66ae81d18 dev test baseurl for test
Test Workflow / test (push) Has been cancelled
2026-04-21 19:34:43 +07:00
backdev 6c3742913c dev test baseurl for test
Test Workflow / test (push) Has been cancelled
2026-04-21 19:09:27 +07:00
backdev dfc9a22bc5 dev test baseurl for test
Test Workflow / test (push) Has been cancelled
2026-04-21 18:11:36 +07:00
backdev d0be6580e4 dev test baseurl for test
Test Workflow / test (push) Has been cancelled
2026-04-21 11:50:25 +07:00
backdev 1dd6a6b7e2 dev test baseurl for test
Test Workflow / test (push) Has been cancelled
2026-04-21 11:01:15 +07:00
backdev c33ffe516a dev test baseurl for test
Test Workflow / test (push) Has been cancelled
2026-04-21 10:43:13 +07:00
backdev ac7cc54b5e dev test baseurl for test
Test Workflow / test (push) Has been cancelled
2026-04-21 10:22:42 +07:00
backdev ea7dd5e5df dev test baseurl for test
Test Workflow / test (push) Has been cancelled
2026-04-21 10:11:31 +07:00
backdev c2a649a252 dev test baseurl for test
Test Workflow / test (push) Has been cancelled
2026-04-21 09:26:57 +07:00
backdev f680f766cd dev test baseurl for test
Test Workflow / test (push) Has been cancelled
2026-04-21 09:26:03 +07:00
backdev 82827c9a2a dev test baseurl for test
Test Workflow / test (push) Has been cancelled
2026-04-21 03:45:23 +07:00
backdev e6e8461b9b dev test baseurl for test
Test Workflow / test (push) Has been cancelled
2026-04-21 03:27:47 +07:00
backdev 1b2337ea2f dev fix thumbnal path
Test Workflow / test (push) Has been cancelled
2026-04-21 03:11:52 +07:00
backdev 5fc9563a4e dev delete check file on disk
Test Workflow / test (push) Has been cancelled
2026-04-21 02:59:21 +07:00
backdev fdc17b8325 dev delete check file on disk
Test Workflow / test (push) Has been cancelled
2026-04-21 02:21:33 +07:00
backdev 3b68b3337c dev delete check file on disk
Test Workflow / test (push) Has been cancelled
2026-04-21 02:10:17 +07:00
backdev 631066b217 dev delete check file on disk
Test Workflow / test (push) Has been cancelled
2026-04-21 01:52:48 +07:00
backdev 61dcada973 dev delete check file on disk
Test Workflow / test (push) Has been cancelled
2026-04-21 01:43:40 +07:00
backdev 7492b8aa7f dev fix file public endpoint
Test Workflow / test (push) Has been cancelled
2026-04-21 01:31:30 +07:00
backdev 9ad4830edb dev fix file public endpoint
Test Workflow / test (push) Has been cancelled
2026-04-21 01:06:50 +07:00
backdev 6366cbc96b dev change
Test Workflow / test (push) Has been cancelled
2026-04-21 00:36:22 +07:00
backdev 4e0e953253 dev change
Test Workflow / test (push) Has been cancelled
2026-04-20 19:50:32 +07:00
backdev 1476509b48 dev change
Test Workflow / test (push) Has been cancelled
2026-04-20 18:50:47 +07:00
backdev 9cde6f3bb6 dev change logic for get files for search
Test Workflow / test (push) Has been cancelled
2026-04-20 18:31:32 +07:00
backdev 9cd5965b37 dev change logic for get files for search
Test Workflow / test (push) Has been cancelled
2026-04-20 18:16:46 +07:00
backdev 2ab91dee58 dev fix save to storage
Test Workflow / test (push) Has been cancelled
2026-04-20 14:26:10 +07:00
vladp b0cc5720a6 dev add statistic files protected
Test Workflow / test (push) Successful in 5s
2026-04-16 17:32:49 +07:00
vladp ed89d315ae Merge branch 'dev' into NCBACK-35
# Conflicts:
#	src/main/java/ru/soune/nocopy/controller/ApiController.java
#	src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java
#	src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java
2026-04-16 16:25:36 +07:00
vladp 74f9be8bf7 dev add statistic files protected
Test Workflow / test (push) Successful in 2s
2026-04-16 01:08:40 +07:00
vladp bbf8e3ee3d dev add statistic files protected
Test Workflow / test (push) Successful in 2s
2026-04-16 00:54:44 +07:00
vladp db3dcb8166 dev add statistic files protected
Test Workflow / test (push) Successful in 2s
2026-04-16 00:37:24 +07:00
vladp 42a5368003 dev add statistic files protected
Test Workflow / test (push) Successful in 2s
2026-04-16 00:28:27 +07:00
vladp be62dd458a dev add statistic files protected
Test Workflow / test (push) Successful in 3s
2026-04-15 18:28:42 +07:00
vladp 2d80b40b27 dev add statistic files protected
Test Workflow / test (push) Successful in 3s
2026-04-15 18:12:32 +07:00
vladp 1b921b525a dev add statistic rest
Test Workflow / test (push) Successful in 2s
2026-04-15 17:46:53 +07:00
vladp b56fb63a37 dev add statistic rest
Test Workflow / test (push) Successful in 2s
2026-04-15 17:41:33 +07:00
vladp bb41898abf dev add statistic rest
Test Workflow / test (push) Successful in 3s
2026-04-15 17:32:07 +07:00
vladp 9974e22abe dev add
Test Workflow / test (push) Successful in 3s
2026-04-15 14:26:46 +07:00
vladp 064a888188 dev add
Test Workflow / test (push) Successful in 3s
2026-04-15 12:49:03 +07:00
vladp 484475eadc dev add nexus in docker-compose
Test Workflow / test (push) Successful in 2s
2026-04-15 10:58:43 +07:00
vladp c60aeec6f5 dev add geo country service
Test Workflow / test (push) Successful in 3s
2026-04-15 10:25:25 +07:00
vladp 0bb952c4e6 dev add geo country service
Test Workflow / test (push) Successful in 3s
2026-04-15 10:17:47 +07:00
vladp 11ee4e18ed dev add geo country service
Test Workflow / test (push) Successful in 3s
2026-04-15 10:10:46 +07:00
vladp 684888aa2c dev add geo country service
Test Workflow / test (push) Successful in 3s
2026-04-15 09:59:38 +07:00
vladp eb43621b74 dev add geo country service
Test Workflow / test (push) Successful in 3s
2026-04-15 09:51:18 +07:00
vladp dd73227db1 dev add geo country service
Test Workflow / test (push) Successful in 2s
2026-04-15 09:46:29 +07:00
vladp a7939845c7 dev add geo country service
Test Workflow / test (push) Successful in 3s
2026-04-15 09:34:32 +07:00
vladp 40cb53a3b0 dev update mongodb for ci/cd
Test Workflow / test (push) Successful in 2s
2026-04-14 18:35:21 +07:00
vladp bac45ec738 dev update mongodb for ci/cd
Test Workflow / test (push) Successful in 2s
2026-04-14 18:26:23 +07:00
vladp 01cfc16583 dev add mongodb container
Test Workflow / test (push) Successful in 3s
2026-04-14 17:32:43 +07:00
vladp f71252bb5e dev change mod user status enum
Test Workflow / test (push) Successful in 2s
2026-04-14 16:44:58 +07:00
vladp 934f3459fb dev change mod user status enum
Test Workflow / test (push) Successful in 3s
2026-04-14 12:57:14 +07:00
vladp 75643ae84c dev change sort user all files
Test Workflow / test (push) Successful in 3s
2026-04-14 11:06:07 +07:00
vladp 7df791f9b8 dev change sort user all files
Test Workflow / test (push) Successful in 16s
2026-04-13 19:19:34 +07:00
vladp b518504e58 dev change sort user all files
Test Workflow / test (push) Successful in 5s
2026-04-13 19:09:30 +07:00
vladp 3558588da9 dev change sort user all files
Test Workflow / test (push) Successful in 4s
2026-04-13 18:47:28 +07:00
vladp 6e8ada2429 dev change sort user all files
Test Workflow / test (push) Successful in 4s
2026-04-13 18:38:10 +07:00
vladp 64dce8601a dev change sort user all files
Test Workflow / test (push) Successful in 12s
2026-04-13 18:25:33 +07:00
vladp 86fa3b555a dev change sort user all files
Test Workflow / test (push) Successful in 4s
2026-04-13 18:20:38 +07:00
vladp e089d370cf dev change sort user all files
Test Workflow / test (push) Successful in 4s
2026-04-13 18:03:03 +07:00
vladp 28ddf49af1 dev change sort user all files
Test Workflow / test (push) Successful in 4s
2026-04-13 17:54:46 +07:00
vladp 93946cc2e3 dev change sort user all files
Test Workflow / test (push) Successful in 4s
2026-04-13 17:45:48 +07:00
vladp 671625e0cf dev change sort user all files
Test Workflow / test (push) Successful in 4s
2026-04-13 17:30:32 +07:00
vladp 40c9b2a796 dev change sort user all files
Test Workflow / test (push) Successful in 5s
2026-04-13 17:16:32 +07:00
vladp b859b175f0 dev change user status after download passports
Test Workflow / test (push) Successful in 4s
2026-04-13 15:41:25 +07:00
vladp d827e2ca16 dev change sort for monitoring
Test Workflow / test (push) Successful in 4s
2026-04-13 15:35:49 +07:00
vladp 1ae2819245 dev change defolt sort
Test Workflow / test (push) Successful in 11s
2026-04-13 15:06:00 +07:00
vladp dbefc21e6d dev add token spent logic
Test Workflow / test (push) Successful in 5s
2026-04-13 15:04:22 +07:00
vladp e32e2d990b dev add token spent logic
Test Workflow / test (push) Successful in 5s
2026-04-13 14:20:13 +07:00
vladp 3896477783 dev add token spent logic
Test Workflow / test (push) Successful in 5s
2026-04-13 14:04:19 +07:00
vladp b5d4509769 dev add token spent logic
Test Workflow / test (push) Waiting to run
2026-04-13 12:15:44 +07:00
vladp aa6c0c1b94 dev add token spent logic
Test Workflow / test (push) Successful in 4s
2026-04-10 20:13:59 +07:00
vladp 31140e810b dev delete async
Test Workflow / test (push) Successful in 3s
2026-04-09 11:03:31 +07:00
vladp aef2c1dfe0 dev add verified notifications
Test Workflow / test (push) Successful in 1m29s
2026-04-08 21:58:28 +07:00
vladp c1360fce87 dev add verified notifications
Test Workflow / test (push) Successful in 6s
2026-04-08 11:31:49 +07:00
vladp 9e4035bf83 dev add violation link
Test Workflow / test (push) Successful in 3s
2026-04-07 18:09:14 +07:00
vladp b5de6c04f7 dev add violation link
Test Workflow / test (push) Successful in 2s
2026-04-07 16:59:30 +07:00
vladp a8a7a78ebf dev add violation link
Test Workflow / test (push) Successful in 2s
2026-04-07 16:43:08 +07:00
vladp 29ed74745f dev add violation link
Test Workflow / test (push) Successful in 4s
2026-04-07 15:35:32 +07:00
vladp 9e81b45922 dev change key
Test Workflow / test (push) Successful in 4s
2026-04-07 14:42:02 +07:00
vladp be1ad7f066 dev add notification entity
Test Workflow / test (push) Has been cancelled
2026-04-04 11:26:35 +07:00
vladp 7cd2aa7d32 Merge branch 'dev' into NCBACK-35
# Conflicts:
#	src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java
2026-04-04 11:23:05 +07:00
vladp 69e758a710 dev email send when tokens not found
Test Workflow / test (push) Successful in 3s
2026-03-31 14:43:34 +07:00
vladp d7874aa37a Merge branch 'dev' into NCBACK-35
# Conflicts:
#	README.md
#	src/main/java/ru/soune/nocopy/controller/ApiController.java
#	src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java
#	src/main/java/ru/soune/nocopy/service/file/FileCleanupService.java
#	src/main/java/ru/soune/nocopy/service/file/FileEntityService.java
#	src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java
#	src/main/java/ru/soune/nocopy/service/file/impl/ProtectionFileProviderImpl.java
2026-03-31 14:24:03 +07:00
vladp daef30ec11 NCBACK-35 add worked logic for save protected files
Test Workflow / test (push) Successful in 4s
2026-02-16 16:05:15 +07:00
vladp a25b33fe13 NCBACK-35 add worked logic for save protected files
Test Workflow / test (push) Successful in 4s
2026-02-16 15:04:58 +07:00
vladp 2985d84b04 NCBACK-35 add cloud for save when download last chunk and after protected
Test Workflow / test (push) Successful in 3s
2026-02-13 21:11:30 +07:00
96 changed files with 3208 additions and 306 deletions
+13 -2
View File
@@ -290,7 +290,7 @@ server_name dev-admin.not-copy.com;
ssl_certificate_key /etc/letsencrypt/live/dev-admin.not-copy.com/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/dev-admin.not-copy.com/privkey.pem;
location /api/ { location /api/ {
proxy_pass http://localhost:3001; proxy_pass http://localhost:8082;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@@ -323,6 +323,7 @@ server_name dev-workspace.not-copy.com;
ssl_certificate_key /etc/letsencrypt/live/dev-workspace.not-copy.com/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/dev-workspace.not-copy.com/privkey.pem;
location /api/ { location /api/ {
proxy_pass http://localhost:3001; proxy_pass http://localhost:3001;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
@@ -489,4 +490,14 @@ killall 3proxy # Остановка прокси
На основном сервере: На основном сервере:
bash bash
curl -x http://193.46.217.94:3128 https://api.ipify.org # Проверка прокси curl -x http://193.46.217.94:3128 https://api.ipify.org # Проверка прокси
telnet 193.46.217.94 3128 # Проверка порта telnet 193.46.217.94 3128 # Проверка порта
------
Настройка Яндекс Клауд
1. YCAJEpWAtaVkVGX0sH6_EupEg - индетификатор ключа
2. YCMmVykfXrZ_nfU13Vo4yoCVGa70DnTlBgF1pUzO - секретный ключ
private static final String KEY_ID = "YCAJEmHZcWxzf4oGWvETG3mms";
private static final String SECRET_KEY = "YCPAPZ32XC-LWb8xE0b_xTcBD8NZUCHpSOfu8LUG";
+7
View File
@@ -62,9 +62,16 @@ dependencies {
implementation name: 'testlib-fat-0.3.1-all' implementation name: 'testlib-fat-0.3.1-all'
implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.12.0' implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.12.0'
implementation 'com.maxmind.geoip2:geoip2:4.2.0'
implementation project(':referral') implementation project(':referral')
//cloud
implementation("software.amazon.awssdk:aws-sdk-java:2.29.33")
implementation("software.amazon.awssdk:apache-client:2.29.33")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0")
implementation("org.springframework.boot:spring-boot-starter-web")
} }
tasks.named('test') { tasks.named('test') {
+31
View File
@@ -216,6 +216,33 @@ services:
networks: networks:
- app-network - app-network
nexus:
image: sonatype/nexus3:latest
container_name: nexus
restart: unless-stopped
ports:
- "9000:8081"
volumes:
- nexus-data:/nexus-data
environment:
- INSTALL4J_ADD_VM_PARAMS=-Xms512m -Xmx1024m -XX:MaxDirectMemorySize=512m
networks:
- app-network
mongodb:
image: mongo:latest
container_name: mongo_ncp
restart: unless-stopped
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: mongo_db
MONGO_INITDB_ROOT_PASSWORD: mongo_ncp
volumes:
- mongodb_data:/data/db
networks:
- app-network
tempo: tempo:
image: grafana/tempo:2.4.1 image: grafana/tempo:2.4.1
container_name: tempo container_name: tempo
@@ -267,6 +294,8 @@ services:
volumes: volumes:
pgdata: pgdata:
mongodb_data:
name: mongodb_data
artifacts: artifacts:
tempo_data: tempo_data:
loki_data: loki_data:
@@ -275,6 +304,8 @@ volumes:
loki_rules: loki_rules:
uploads_data: uploads_data:
prometheus_data: prometheus_data:
nexus-data:
driver: local
# postfix-data: # postfix-data:
networks: networks:
app-network: app-network:
+23 -8
View File
@@ -86,10 +86,11 @@ pipeline {
passwordVariable: 'SSH_PASS' passwordVariable: 'SSH_PASS'
), ),
string(credentialsId: 'DB_USER', variable: 'DB_USER'), string(credentialsId: 'DB_USER', variable: 'DB_USER'),
string(credentialsId: 'DB_PASSWORD', variable: 'DB_PASSWORD') string(credentialsId: 'DB_PASSWORD', variable: 'DB_PASSWORD'),
string(credentialsId: 'MONGO_USER', variable: 'MONGO_USER'),
string(credentialsId: 'MONGO_PASSWORD', variable: 'MONGO_PASSWORD')
]) { ]) {
sh """ sh """
# Тестируем подключение сначала
echo "Testing SSH connection..." echo "Testing SSH connection..."
if sshpass -p '${SSH_PASS}' ssh -o StrictHostKeyChecking=no ${SSH_USER}@${params.SERVER} 'echo "SSH connection successful"'; then if sshpass -p '${SSH_PASS}' ssh -o StrictHostKeyChecking=no ${SSH_USER}@${params.SERVER} 'echo "SSH connection successful"'; then
echo "SSH connection OK" echo "SSH connection OK"
@@ -98,7 +99,6 @@ pipeline {
exit 1 exit 1
fi fi
# Основная команда
sshpass -p '${SSH_PASS}' ssh -o StrictHostKeyChecking=no ${SSH_USER}@${params.SERVER} " sshpass -p '${SSH_PASS}' ssh -o StrictHostKeyChecking=no ${SSH_USER}@${params.SERVER} "
mkdir -p /opt/deployments/${params.BRANCH}-${params.PORT} mkdir -p /opt/deployments/${params.BRANCH}-${params.PORT}
cd /opt/deployments/${params.BRANCH}-${params.PORT} cd /opt/deployments/${params.BRANCH}-${params.PORT}
@@ -118,6 +118,8 @@ pipeline {
docker rm postgres-${params.PORT} 2>/dev/null || echo 'Контейнер БД не найден' docker rm postgres-${params.PORT} 2>/dev/null || echo 'Контейнер БД не найден'
docker stop file-storage-${params.PORT} 2>/dev/null || echo 'Контейнер storage не найден' docker stop file-storage-${params.PORT} 2>/dev/null || echo 'Контейнер storage не найден'
docker rm file-storage-${params.PORT} 2>/dev/null || echo 'Контейнер storage не найден' docker rm file-storage-${params.PORT} 2>/dev/null || echo 'Контейнер storage не найден'
docker stop mongodb-${params.PORT} 2>/dev/null || echo 'Контейнер MongoDB не найден'
docker rm mongodb-${params.PORT} 2>/dev/null || echo 'Контейнер MongoDB не найден'
echo '3. Запускаем БД...' echo '3. Запускаем БД...'
docker run -d \\ docker run -d \\
@@ -134,20 +136,31 @@ pipeline {
-e BUILD_TIME_BACK='${buildTime}' \\ -e BUILD_TIME_BACK='${buildTime}' \\
postgres:17.7 postgres:17.7
echo '4. Запускаем storage...' echo '4. Запускаем MongoDB...'
docker run -d \\
--name mongodb-${params.PORT} \\
--network ${params.NETWORK} \\
--network-alias mongodb-${params.PORT} \\
-p ${params.PORT}1:27017 \\
-e MONGO_INITDB_ROOT_USERNAME=${MONGO_USER} \\
-e MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD} \\
-v mongodb_data_${params.PORT}:/data/db \\
mongo:7.0
echo '5. Запускаем storage...'
docker run -d \\ docker run -d \\
--name file-storage-${params.PORT} \\ --name file-storage-${params.PORT} \\
--network ${params.NETWORK} \\ --network ${params.NETWORK} \\
-v uploads_data_${params.PORT}:/storage:rw \\ -v uploads_data_${params.PORT}:/storage:rw \\
alpine:latest tail -f /dev/null alpine:latest tail -f /dev/null
echo '5. Ждем БД...' echo '6. Ждем БД...'
sleep 15 sleep 15
echo '6. Собираем приложение...' echo '7. Собираем приложение...'
docker build --no-cache -t app-backend-${params.PORT}:latest . docker build --no-cache -t app-backend-${params.PORT}:latest .
echo '7. Запускаем приложение...' echo '8. Запускаем приложение...'
docker run -d \\ docker run -d \\
--name app-backend-${params.PORT} \\ --name app-backend-${params.PORT} \\
--network ${params.NETWORK} \\ --network ${params.NETWORK} \\
@@ -162,9 +175,11 @@ pipeline {
-e POSTGRES_HOST=no_copy_${params.PORT} \\ -e POSTGRES_HOST=no_copy_${params.PORT} \\
-e FILE_STORAGE_PATH=/data/uploads \\ -e FILE_STORAGE_PATH=/data/uploads \\
-e SPRING_PROFILES_ACTIVE=${springProfile} \\ -e SPRING_PROFILES_ACTIVE=${springProfile} \\
-e MONGO_URI=mongodb://${MONGO_USER}:${MONGO_PASSWORD}@mongodb-${params.PORT}:27017 \\
-e MONGO_DATABASE=no_copy \\
app-backend-${params.PORT}:latest app-backend-${params.PORT}:latest
echo '8. Проверка...' echo '9. Проверка...'
sleep 10 sleep 10
if curl -s -f http://localhost:${params.PORT}/health > /dev/null 2>&1; then if curl -s -f http://localhost:${params.PORT}/health > /dev/null 2>&1; then
@@ -37,7 +37,17 @@ public class HandlerConfig {
UserInfoHandler userInfoHandler, UserInfoHandler userInfoHandler,
NotificationHandler notificationHandler, NotificationHandler notificationHandler,
UserVerificationHandler userVerificationHandler, UserVerificationHandler userVerificationHandler,
LawCaseHandler lawCaseHandler LawCaseHandler lawCaseHandler,
TokenOperationHandler tokenOperationHandler,
StatisticUserFilesHandler statisticUserHandler,
StatisticTariffInfoFileHandler statisticTariffInfoFileHandler,
StatisticUserDynamicHandler statisticUserDynamicHandler,
StatisticProtectedFilesHandler statisticProtectedFilesHandler,
StatisticViolationHandler statisticViolationHandler,
StatisticSubscriberHandler statisticSubscriberHandler,
StatisticTokenHandler statisticTokenHandler,
StatisticIncomeHandler statisticIncomeHandler
) { ) {
Map<Integer, RequestHandler> map = new HashMap<>(); Map<Integer, RequestHandler> map = new HashMap<>();
map.put(20001, login); map.put(20001, login);
@@ -66,6 +76,15 @@ public class HandlerConfig {
map.put(30015, notificationHandler); map.put(30015, notificationHandler);
map.put(30016, userVerificationHandler); map.put(30016, userVerificationHandler);
map.put(30017, lawCaseHandler); map.put(30017, lawCaseHandler);
map.put(30018, tokenOperationHandler);
map.put(30019, statisticUserHandler);
map.put(30020, statisticTariffInfoFileHandler);
map.put(30021, statisticUserDynamicHandler);
map.put(30022, statisticProtectedFilesHandler);
map.put(30023, statisticViolationHandler);
map.put(30024, statisticSubscriberHandler);
map.put(30025, statisticTokenHandler);
map.put(30026, statisticIncomeHandler);
return map; return map;
} }
@@ -1,5 +1,7 @@
package ru.soune.nocopy.controller; package ru.soune.nocopy.controller;
import com.google.api.client.util.IOUtils;
import com.google.common.io.ByteStreams;
import com.vrt.NoCopyFileService; import com.vrt.NoCopyFileService;
import com.vrt.fileprotection.FileProtector; import com.vrt.fileprotection.FileProtector;
import com.vrt.fileprotection.NoCopyCheckResult; import com.vrt.fileprotection.NoCopyCheckResult;
@@ -8,16 +10,20 @@ import com.vrt.fileprotection.documents.DocumentCheckResult;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.InputStreamResource;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault; import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError; import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import ru.soune.nocopy.dto.BaseRequest; import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.MessageCode;
@@ -36,12 +42,17 @@ import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.FileSimilarityService; import ru.soune.nocopy.service.FileSimilarityService;
import ru.soune.nocopy.service.file.*; import ru.soune.nocopy.service.file.*;
import ru.soune.nocopy.service.file.CheckCounterService;
import ru.soune.nocopy.service.file.ProtectionsLimitService;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.user.moderation.UserVerificationService; import ru.soune.nocopy.service.user.moderation.UserVerificationService;
import ru.soune.nocopy.util.FileUtil; import ru.soune.nocopy.util.FileUtil;
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
@@ -65,6 +76,8 @@ public class ApiController {
private final FileEntityRepository fileEntityRepository; private final FileEntityRepository fileEntityRepository;
private final NoCopyFileService noCopyFileService;
private final FileUtil fileUtil; private final FileUtil fileUtil;
private final ProtectionsLimitService protectionsLimitService; private final ProtectionsLimitService protectionsLimitService;
@@ -75,6 +88,8 @@ public class ApiController {
private final CheckCounterService checkCounterService; private final CheckCounterService checkCounterService;
private final CloudStorageService cloudStorageService;
private final ZipService zipService; private final ZipService zipService;
private final UserVerificationService userVerificationService; private final UserVerificationService userVerificationService;
@@ -159,7 +174,7 @@ public class ApiController {
MessageCode.DUPLICATE_FILE_UPLOAD.getDescription(), MessageCode.DUPLICATE_FILE_UPLOAD.getDescription(),
duplicateData duplicateData
)); ));
}catch (FileFormatException e){ } catch (FileFormatException e){
return buildErrorResponse(uploadId, chunkNumber, e.getMessage()); return buildErrorResponse(uploadId, chunkNumber, e.getMessage());
} catch (Exception e) { } catch (Exception e) {
log.error("Error uploading chunk", e); log.error("Error uploading chunk", e);
@@ -172,7 +187,7 @@ public class ApiController {
@RequestParam(value = "upload_id", required = false) String uploadId, @RequestParam(value = "upload_id", required = false) String uploadId,
@RequestParam(value = "chunk_number", required = false) Integer chunkNumber, @RequestParam(value = "chunk_number", required = false) Integer chunkNumber,
@RequestParam(value = "chunk", required = false) MultipartFile chunk, @RequestParam(value = "chunk", required = false) MultipartFile chunk,
@RequestParam(value = "last_file") Boolean last_file, @RequestParam(value = "last_file", required = false) Boolean last_file,
@RequestParam(value = "token") String token) { @RequestParam(value = "token") String token) {
try { try {
if (chunk == null || chunk.isEmpty()) { if (chunk == null || chunk.isEmpty()) {
@@ -205,7 +220,17 @@ public class ApiController {
} }
} }
private final NoCopyFileService noCopyFileService; @GetMapping("/check/{fileId}")
public ResponseEntity<BaseResponse> check(@PathVariable String fileId) {
FileEntity fileEntity = fileEntityRepository.findByFileId(fileId);
File file = new File(fileEntity.getFilePath());
NoCopyCheckResult checkResult = noCopyFileService.checkFile(file, FileProtector.Type.IMAGE);
log.info("checkResult: {}", checkResult);
return ResponseEntity.ok(new BaseResponse(1, 1, "1", checkResult));
}
@GetMapping("/v{version}/files/{fileId}/similar") @GetMapping("/v{version}/files/{fileId}/similar")
public ResponseEntity<BaseResponse> findSimilarFiles( public ResponseEntity<BaseResponse> findSimilarFiles(
@@ -477,39 +502,35 @@ public class ApiController {
MessageCode.FILE_DELETE.getDescription(), MessageCode.FILE_DELETE.getDescription(),
errorData)); errorData));
} }
//TODO
if (!entityResponse.isExistsOnDisk()) { // if (!entityResponse.isExistsOnDisk()) {
Map<String, Object> errorData = new HashMap<>(); // Map<String, Object> errorData = new HashMap<>();
errorData.put("onDisk", entityResponse.isExistsOnDisk()); // errorData.put("onDisk", entityResponse.isExistsOnDisk());
//
// return ResponseEntity.ok().body(new BaseResponse(20004,
// MessageCode.FILE_NOT_EXIST.getCode(),
// MessageCode.FILE_NOT_EXIST.getDescription(),
// errorData));
// }
return ResponseEntity.ok().body(new BaseResponse(20004, // if (!file.exists()) {
MessageCode.FILE_NOT_EXIST.getCode(), // Map<String, Object> errorData = new HashMap<>();
MessageCode.FILE_NOT_EXIST.getDescription(), // errorData.put("file", file.exists());
errorData)); //
} // return ResponseEntity.ok().body(new BaseResponse(20004,
// MessageCode.FILE_DOWNLOAD_ERROR.getCode(),
// MessageCode.FILE_DOWNLOAD_ERROR.getDescription(),
// errorData));
// }
byte[] bytes = cloudStorageService.readFileFromStorageBytes(entityResponse.getProtectedFilePath());
Path filePath = Paths.get(entityResponse.getProtectedFilePath());
FileSystemResource resource = new FileSystemResource(filePath);
long fileSize = Files.size(filePath);
if (!resource.exists()) {
Map<String, Object> errorData = new HashMap<>();
errorData.put("resource", resource.exists());
return ResponseEntity.ok().body(new BaseResponse(20004,
MessageCode.FILE_DOWNLOAD_ERROR.getCode(),
MessageCode.FILE_DOWNLOAD_ERROR.getDescription(),
errorData));
}
String contentType = determineContentType(filePath);
return ResponseEntity.ok() return ResponseEntity.ok()
.contentLength(fileSize) .contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION, .header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + entityResponse.getFileName() + "\"") "attachment; filename=\"" + entityResponse.getFileName() + "\"")
.body(resource); .body(bytes);
} catch (FileEntityNotFoundException e) { } catch (FileEntityNotFoundException e) {
Map<String, Object> errorData = new HashMap<>(); Map<String, Object> errorData = new HashMap<>();
errorData.put("fileId", fileId); errorData.put("fileId", fileId);
@@ -526,16 +547,16 @@ public class ApiController {
MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(), MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(),
MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(), MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(),
errorData)); errorData));
} catch (IOException e) { // } catch (IOException e) {
Map<String, Object> errorData = new HashMap<>(); // Map<String, Object> errorData = new HashMap<>();
errorData.put("token", tokenHeader); // errorData.put("token", tokenHeader);
errorData.put("fileId", fileId); // errorData.put("fileId", fileId);
errorData.put("version", version); // errorData.put("version", version);
//
return ResponseEntity.ok().body(new BaseResponse(20004, // return ResponseEntity.ok().body(new BaseResponse(20004,
MessageCode.FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD.getCode(), // MessageCode.FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD.getCode(),
MessageCode.FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD.getDescription(), // MessageCode.FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD.getDescription(),
errorData)); // errorData));
} }
} }
@@ -552,33 +573,6 @@ public class ApiController {
return ResponseEntity.ok().body(fileTypeStats); return ResponseEntity.ok().body(fileTypeStats);
} }
private boolean hasDuplicate(List<SimilarFileDTO> similarFiles) {
return similarFiles.stream().anyMatch(f -> f.getHammingDistance() <= 5);
}
private ResponseEntity<BaseResponse> handleDuplicate(FileEntity fileEntity, List<SimilarFileDTO> similarFiles)
throws IOException {
fileEntityService.deleteFromDisk(fileEntity);
fileEntityService.softDeleteFileWithHash(fileEntity);
Optional<FileEntity> originalFile = fileEntityRepository.findById(similarFiles.get(0).getFileId());
if (originalFile.isPresent()) {
Map<String, String> duplicateInfo = Map.of(
"duplicate_file_id", originalFile.get().getId(),
"owner_user_id", String.valueOf(originalFile.get().getUserId()));
return ResponseEntity.ok().body(new BaseResponse(
20004,
MessageCode.DUPLICATE_FILE_UPLOAD.getCode(),
"Failed to upload chunk, duplicate",
duplicateInfo));
}
return null;
}
private ResponseEntity<BaseResponse> buildSuccessResponse(String uploadId, Integer chunkNumber, MultipartFile chunk, private ResponseEntity<BaseResponse> buildSuccessResponse(String uploadId, Integer chunkNumber, MultipartFile chunk,
String fileId) { String fileId) {
ChunkUploadResponse responseBody = ChunkUploadResponse.builder() ChunkUploadResponse responseBody = ChunkUploadResponse.builder()
@@ -635,13 +629,4 @@ public class ApiController {
return errorDetail; return errorDetail;
} }
private String determineContentType(Path filePath) throws IOException {
String contentType = Files.probeContentType(filePath);
if (contentType == null) {
contentType = "application/octet-stream";
}
return contentType;
}
} }
@@ -1,12 +1,16 @@
package ru.soune.nocopy.controller; package ru.soune.nocopy.controller;
import com.google.api.client.util.IOUtils;
import com.google.common.io.ByteStreams;
import com.vrt.NoCopyFileService; import com.vrt.NoCopyFileService;
import com.vrt.fileprotection.FileProtector; import com.vrt.fileprotection.FileProtector;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.http.*; import org.springframework.http.*;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import ru.soune.nocopy.dto.file.CheckStatus; import ru.soune.nocopy.dto.file.CheckStatus;
import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.ProtectionStatus; import ru.soune.nocopy.entity.file.ProtectionStatus;
@@ -18,13 +22,14 @@ import ru.soune.nocopy.service.file.CheckCounterService;
import ru.soune.nocopy.service.file.FileStorageService; import ru.soune.nocopy.service.file.FileStorageService;
import ru.soune.nocopy.service.file.ImageResizeService; import ru.soune.nocopy.service.file.ImageResizeService;
import ru.soune.nocopy.service.file.ZipService; import ru.soune.nocopy.service.file.ZipService;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import ru.soune.nocopy.util.FileUtil; import ru.soune.nocopy.util.FileUtil;
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
import java.io.FileNotFoundException; import java.io.*;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
@@ -51,19 +56,13 @@ public class FileController {
private ZipService zipService; private ZipService zipService;
private CloudStorageService cloudStorageService;
@GetMapping("/public/{fileId}") @GetMapping("/public/{fileId}")
public ResponseEntity<Resource> getPublicFile(@PathVariable String fileId) { public ResponseEntity<StreamingResponseBody> getPublicFile(@PathVariable String fileId) {
try { try {
FileEntity fileEntity = fileRepository.findById(fileId) FileEntity fileEntity = fileRepository.findById(fileId)
.orElseThrow(() -> new FileNotFoundException("Not found: " + fileId)); .orElseThrow(() -> new FileNotFoundException("Not found in DB: " + fileId));
Path filePath = Paths.get(fileEntity.getFilePath());
if (!Files.exists(filePath)) {
log.error("Not on disk: {}", fileEntity.getFilePath());
return ResponseEntity.notFound().build();
}
Resource resource = fileStorageService.loadFileAsResource(fileEntity.getFilePath());
MediaType mediaType = getMediaType(fileEntity); MediaType mediaType = getMediaType(fileEntity);
@@ -71,18 +70,28 @@ public class FileController {
.filename(fileEntity.getOriginalFileName(), StandardCharsets.UTF_8) .filename(fileEntity.getOriginalFileName(), StandardCharsets.UTF_8)
.build(); .build();
StreamingResponseBody responseBody = outputStream -> {
try (InputStream inputStream = cloudStorageService.readFileFromStorage(fileEntity.getFilePath())) {
ByteStreams.copy(inputStream, outputStream);
} catch (NoSuchKeyException e) {
log.error("File disappeared from S3 during streaming: {}", fileId, e);
throw new IOException("File not found in storage", e);
}
};
return ResponseEntity.ok() return ResponseEntity.ok()
.contentType(mediaType) .contentType(mediaType)
.contentLength(Files.size(filePath))
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition.toString()) .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition.toString())
.header(HttpHeaders.CACHE_CONTROL, "public, max-age=3600") .header(HttpHeaders.CACHE_CONTROL, "public, max-age=3600")
.body(resource); .body(responseBody);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
log.warn("Not on disk: {}", fileId); log.warn("File not found in DB: {}", fileId);
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
} catch (IOException e) { } catch (NoSuchKeyException e) {
log.error("Read file exception: {}", fileId, e); log.warn("File not found in S3: {}", fileId);
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
} catch (Exception e) {
log.error("Error reading file: {}", fileId, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
} }
} }
@@ -113,9 +122,14 @@ public class FileController {
FileEntity fileEntity = fileRepository.findById(fileId) FileEntity fileEntity = fileRepository.findById(fileId)
.orElseThrow(() -> new RuntimeException("File not found")); .orElseThrow(() -> new RuntimeException("File not found"));
String path = imageResizeService.getPath(fileEntity, type); String filePath = type.equals("thumbnail") ? fileEntity.getThumbnailPath(): fileEntity.getFilePath();
Resource resource = fileStorageService.loadFileAsResource(path); // InputStream file = cloudStorageService.readFileFromStorage(filePath);
// if (file == null || !file.exists()) {
// log.error("File not found in storage: {}", fileEntity.getFilePath());
// return ResponseEntity.notFound().build();
// }
if (fileEntity.getProtectionStatus() == ProtectionStatus.NOT_PROTECTED || if (fileEntity.getProtectionStatus() == ProtectionStatus.NOT_PROTECTED ||
fileEntity.getProtectionStatus() == ProtectionStatus.PROCESSING) { fileEntity.getProtectionStatus() == ProtectionStatus.PROCESSING) {
@@ -128,6 +142,8 @@ public class FileController {
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8) String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8)
.replace("+", "%20"); .replace("+", "%20");
InputStreamResource resource = new InputStreamResource(cloudStorageService.readFileFromStorage(filePath));
return ResponseEntity.ok() return ResponseEntity.ok()
.contentType(new MediaType(fileEntity.getMimeType(), fileEntity.getFileExtension())) .contentType(new MediaType(fileEntity.getMimeType(), fileEntity.getFileExtension()))
.header(HttpHeaders.CONTENT_DISPOSITION, .header(HttpHeaders.CONTENT_DISPOSITION,
@@ -70,7 +70,7 @@ public class PaymentController {
} }
paymentService.changeAutoRenewal(authService.useUserAuthToken(tokenHeader), renewal); paymentService.changeAutoRenewal(authService.useUserAuthToken(tokenHeader), renewal);
return ResponseEntity.ok(Map.of("message", "Auto-renewal disabled")); return ResponseEntity.ok(Map.of("message", "Auto-renewal changed:" + renewal));
} catch (UserNotFoundException e) { } catch (UserNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND) return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(Map.of("error", e.getMessage())); .body(Map.of("error", e.getMessage()));
@@ -59,7 +59,7 @@ public class UserController {
.map(u -> new UserDTO(u.getFullName(), u.getCompanyName(), u.getEmail(), u.isActive(), .map(u -> new UserDTO(u.getFullName(), u.getCompanyName(), u.getEmail(), u.isActive(),
u.getPhone(), u.getGenderType(), u.getPhone(), u.getGenderType(),
u.getBirthday(), u.getCreatedAt(), u.getSubscriptionType(), tariffService.getAllTariffs(), u.getBirthday(), u.getCreatedAt(), u.getSubscriptionType(), tariffService.getAllTariffs(),
null, null)) null, null, u.getVerificationStatus()))
.toList(); .toList();
return ResponseEntity.ok(allUsers); return ResponseEntity.ok(allUsers);
@@ -79,6 +79,7 @@ public class UserController {
UserDTO userDTO = userMapper.toDTO(user); UserDTO userDTO = userMapper.toDTO(user);
userDTO.setEmail(email); userDTO.setEmail(email);
userDTO.setFullName(user.getFullName()); userDTO.setFullName(user.getFullName());
userDTO.setVerifiedStatus(user.getVerificationStatus());
if (user.getCompany() != null) { if (user.getCompany() != null) {
userDTO.setCompany(user.getCompany().getCompanyName()); userDTO.setCompany(user.getCompany().getCompanyName());
@@ -48,6 +48,7 @@ public enum MessageCode {
TARIFF_IS_NOT_FOUND(0, "Tariff is not found"), TARIFF_IS_NOT_FOUND(0, "Tariff is not found"),
VALIDATION_ERROR(2, "Validation error"), VALIDATION_ERROR(2, "Validation error"),
RESOURCE_NOT_FOUND(4, "Resource not found"), RESOURCE_NOT_FOUND(4, "Resource not found"),
OPERATION_NOT_FOUND(4, "Operation not found"),
INTERNAL_ERROR(4, "Internal server error"), INTERNAL_ERROR(4, "Internal server error"),
COMPANY_NOT_FOUND(4, "Company not found"), COMPANY_NOT_FOUND(4, "Company not found"),
PAYMENT_NOT_FOUND(4, "Payment not found"), PAYMENT_NOT_FOUND(4, "Payment not found"),
@@ -1,5 +1,6 @@
package ru.soune.nocopy.dto.complaint; package ru.soune.nocopy.dto.complaint;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data; import lombok.Data;
import ru.soune.nocopy.entity.complaint.LawCasePriority; import ru.soune.nocopy.entity.complaint.LawCasePriority;
import ru.soune.nocopy.entity.complaint.LawCaseType; import ru.soune.nocopy.entity.complaint.LawCaseType;
@@ -14,17 +15,19 @@ public class LawCaseRequest {
//required //required
private String description; private String description;
private BigDecimal amount; private BigDecimal amount;
private String priority = LawCasePriority.MIDDLE.toString(); private String priority = LawCasePriority.HIGH.toString();
private String type; private String type;
private String lawyer; private String lawyer;
private String token; private String token;
private Integer pageSize = 10; private Integer pageSize = 5;
private Integer pageNumber = 0; private Integer pageNumber = 0;
private String sortBy = "createdAt"; private String sortBy = "updatedAt";
private String sortDir = "asc"; private String sortDir = "desc";
private String action; private String action;
private LawCaseType filterType; private LawCaseType filterType;
private String filterLawyer; private String filterLawyer;
private LawCasePriority filterPriority; private LawCasePriority filterPriority;
@JsonProperty("violation_id")
private Long violationId;
} }
@@ -26,6 +26,7 @@ public class LawCaseResponse {
private int pageSize; private int pageSize;
private long totalElements; private long totalElements;
private int totalPages; private int totalPages;
private long violationId;
private List<LawCaseResponse> content; private List<LawCaseResponse> content;
public static LawCaseResponse fromEntity(LawCase lawCase) { public static LawCaseResponse fromEntity(LawCase lawCase) {
@@ -39,6 +40,7 @@ public class LawCaseResponse {
.lawyer(lawCase.getLawyer()) .lawyer(lawCase.getLawyer())
.createdAt(lawCase.getCreatedAt()) .createdAt(lawCase.getCreatedAt())
.updatedAt(lawCase.getUpdatedAt()) .updatedAt(lawCase.getUpdatedAt())
.violationId(lawCase.getViolationId())
.build(); .build();
} }
} }
@@ -0,0 +1,6 @@
package ru.soune.nocopy.dto.statistic;
import lombok.Data;
@Data
public class IncomeStatisticRequest {
}
@@ -0,0 +1,40 @@
package ru.soune.nocopy.dto.statistic;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class IncomeStatisticResponse {
@JsonProperty("total_income")
private Long totalIncome;
@JsonProperty("total_available")
private Long totalAvailable;
@JsonProperty("total_hold")
private Long totalHold;
@JsonProperty("income_per_user")
private Percentiles incomePerUser;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Percentiles {
@JsonProperty("p5")
private Long p5;
@JsonProperty("p50")
private Long p50;
@JsonProperty("p95")
private Long p95;
}
}
@@ -0,0 +1,7 @@
package ru.soune.nocopy.dto.statistic;
import lombok.Data;
@Data
public class ProtectedFilesStatisticRequest {
}
@@ -0,0 +1,66 @@
package ru.soune.nocopy.dto.statistic;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ProtectedFilesStatisticResponse {
@JsonProperty("total_files")
private Long totalFiles;
@JsonProperty("total_size_bytes")
private Long totalSizeBytes;
@JsonProperty("total_size_gb")
private Double totalSizeGb;
@JsonProperty("files_per_user_percentiles")
private Percentiles filesPerUser;
@JsonProperty("size_per_user_percentiles")
private SizePercentiles sizePerUser;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Percentiles {
@JsonProperty("p5")
private Long p5;
@JsonProperty("p50")
private Long p50;
@JsonProperty("p95")
private Long p95;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class SizePercentiles {
@JsonProperty("p5_bytes")
private Long p5;
@JsonProperty("p50_bytes")
private Long p50;
@JsonProperty("p95_bytes")
private Long p95;
@JsonProperty("p5_mb")
private Double p5Mb;
@JsonProperty("p50_mb")
private Double p50Mb;
@JsonProperty("p95_mb")
private Double p95Mb;
}
}
@@ -0,0 +1,7 @@
package ru.soune.nocopy.dto.statistic;
import lombok.Data;
@Data
public class SubscriberStatisticRequest {
}
@@ -0,0 +1,42 @@
package ru.soune.nocopy.dto.statistic;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SubscriberStatisticResponse {
@JsonProperty("monthly")
private PeriodStatistics monthly;
@JsonProperty("yearly")
private PeriodStatistics yearly;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class PeriodStatistics {
@JsonProperty("current_active_subscriptions")
private Long currentActiveSubscriptions;
@JsonProperty("first_time_subscriptions")
private Long firstTimeSubscriptions;
@JsonProperty("renewals")
private Long renewals;
@JsonProperty("upgrades")
private Long upgrades;
@JsonProperty("downgrades")
private Long downgrades;
}
}
@@ -0,0 +1,17 @@
package ru.soune.nocopy.dto.statistic;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TariffStatisticFullResponse {
@JsonProperty("tariffs")
private List<TariffStatisticResponse> tariffs;
}
@@ -0,0 +1,14 @@
package ru.soune.nocopy.dto.statistic;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class TariffStatisticRequest {
@JsonProperty("company_only")
private Boolean companyOnly = false;
@JsonProperty("active_only")
private Boolean activeOnly = true;
}
@@ -0,0 +1,21 @@
package ru.soune.nocopy.dto.statistic;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TariffStatisticResponse {
@JsonProperty("tariff_name")
private String tariffName;
@JsonProperty("user_count")
private Long userCount;
@JsonProperty("usage_percent")
private Double usagePercent;
}
@@ -0,0 +1,7 @@
package ru.soune.nocopy.dto.statistic;
import lombok.Data;
@Data
public class TokenStatisticRequest {
}
@@ -0,0 +1,40 @@
package ru.soune.nocopy.dto.statistic;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class TokenStatisticResponse {
@JsonProperty("total_bought")
private Long totalBought;
@JsonProperty("total_spent")
private Long totalSpent;
@JsonProperty("bought_per_user")
private Percentiles boughtPerUser;
@JsonProperty("spent_per_user")
private Percentiles spentPerUser;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Percentiles {
@JsonProperty("p5")
private Long p5;
@JsonProperty("p50")
private Long p50;
@JsonProperty("p95")
private Long p95;
}
}
@@ -0,0 +1,7 @@
package ru.soune.nocopy.dto.statistic;
import lombok.Data;
@Data
public class UserDynamicStatisticRequest {
}
@@ -0,0 +1,33 @@
package ru.soune.nocopy.dto.statistic;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.NoArgsConstructor;
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserDynamicStatisticResponse {
@JsonProperty("total_users")
private Long totalUsers;
@JsonProperty("new_last_30_days")
private Long newLast30Days;
@JsonProperty("new_previous_30_days")
private Long newPrevious30Days;
@JsonProperty("growth_percent")
private Double growthPercent;
@JsonProperty("new_current_month")
private Long newCurrentMonth;
@JsonProperty("new_previous_month")
private Long newPreviousMonth;
@JsonProperty("month_growth_percent")
private Double monthGrowthPercent;
}
@@ -0,0 +1,13 @@
package ru.soune.nocopy.dto.statistic;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class UserFilesStatisticRequest {
@JsonProperty("top")
int topUsers;
@JsonProperty("type")
String fileMimeType;
}
@@ -0,0 +1,19 @@
package ru.soune.nocopy.dto.statistic;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class UserFilesStatisticResponse {
@JsonProperty("full_name")
private final String userName;
@JsonProperty("user_id")
private final long userId;
@JsonProperty("count")
private final int countFiles;
@JsonProperty("files_size")
private final long fileSize;
}
@@ -0,0 +1,7 @@
package ru.soune.nocopy.dto.statistic;
import lombok.Data;
@Data
public class ViolationStatisticRequest {
}
@@ -0,0 +1,32 @@
package ru.soune.nocopy.dto.statistic;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ViolationStatisticResponse {
@JsonProperty("total_violations")
private Long totalViolations;
@JsonProperty("total_complaints")
private Long totalComplaints;
@JsonProperty("total_law_cases")
private Long totalLawCases;
@JsonProperty("complaints_without_case")
private Long complaintsWithoutCase;
@JsonProperty("violations_with_complaint")
private Long violationsWithComplaint;
@JsonProperty("violations_without_complaint")
private Long violationsWithoutComplaint;
}
@@ -0,0 +1,34 @@
package ru.soune.nocopy.dto.tokenoperation;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Data;
import ru.soune.nocopy.entity.tokenoperation.TokenOperation;
import java.util.List;
@Data
@Builder
public class TokenOperationPageDto {
@JsonProperty("content")
private List<TokenOperation> content;
@JsonProperty("page_number")
private int pageNumber;
@JsonProperty("page_size")
private int pageSize;
@JsonProperty("total_elements")
private long totalElements;
@JsonProperty("total_pages")
private int totalPages;
@JsonProperty("is_last")
private boolean last;
@JsonProperty("is_first")
private boolean first;
}
@@ -0,0 +1,46 @@
package ru.soune.nocopy.dto.tokenoperation;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import ru.soune.nocopy.entity.tokenoperation.OperationType;
@Data
public class TokenOperationRequest {
@JsonProperty("action")
private String action;
@JsonProperty("token")
private String token;
@JsonProperty("operation_id")
private Long operationId;
@JsonProperty("operation_type")
private OperationType operationType;
@JsonProperty("spent")
private Long spent;
@JsonProperty("page")
private Integer page = 0;
@JsonProperty("size")
private Integer size = 20;
@JsonProperty("sort_by")
private String sortBy = "createdAt";
@JsonProperty("sort_direction")
private String sortDirection = "desc";
@JsonProperty("min_spent")
private Long minSpent;
@JsonProperty("max_spent")
private Long maxSpent;
@JsonProperty("updates")
private Object updates;
}
@@ -0,0 +1,37 @@
package ru.soune.nocopy.dto.tokenoperation;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder;
import lombok.Data;
import java.util.Map;
@Data
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class TokenOperationResponse {
@JsonProperty("success")
private Boolean success;
@JsonProperty("message")
private String message;
@JsonProperty("data")
private Object data;
@JsonProperty("total_elements")
private Long totalElements;
@JsonProperty("total_pages")
private Integer totalPages;
@JsonProperty("current_page")
private Integer currentPage;
@JsonProperty("page_size")
private Integer pageSize;
@JsonProperty("statistics")
private Map<String, Object> statistics;
}
@@ -7,6 +7,7 @@ import lombok.NoArgsConstructor;
import ru.soune.nocopy.dto.tarriff.TariffDTO; import ru.soune.nocopy.dto.tarriff.TariffDTO;
import ru.soune.nocopy.dto.tarriff.TariffInfoDTO; import ru.soune.nocopy.dto.tarriff.TariffInfoDTO;
import ru.soune.nocopy.entity.user.GenderType; import ru.soune.nocopy.entity.user.GenderType;
import ru.soune.nocopy.entity.user.ModerationStatus;
import ru.soune.nocopy.entity.user.SubscriptionType; import ru.soune.nocopy.entity.user.SubscriptionType;
import java.time.LocalDate; import java.time.LocalDate;
@@ -30,4 +31,5 @@ public class UserDTO {
private List<TariffDTO> tariffs; private List<TariffDTO> tariffs;
private TariffInfoDTO tariffInfo; private TariffInfoDTO tariffInfo;
private Long permission; private Long permission;
private ModerationStatus verifiedStatus;
} }
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import ru.soune.nocopy.entity.violation.Violation; import ru.soune.nocopy.entity.violation.Violation;
import ru.soune.nocopy.service.geo.GeoCountryService;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
@@ -12,6 +13,8 @@ import java.util.List;
@Builder @Builder
public class ViolationResponse { public class ViolationResponse {
private final GeoCountryService geoCountryService;
@JsonProperty("violations") @JsonProperty("violations")
private List<ViolationDto> violations; private List<ViolationDto> violations;
@@ -58,6 +61,12 @@ public class ViolationResponse {
@JsonProperty("created_date") @JsonProperty("created_date")
private LocalDateTime createdDate; private LocalDateTime createdDate;
@JsonProperty("country")
private String country;
@JsonProperty("country_code")
private String countryCode;
@JsonProperty("file_id") @JsonProperty("file_id")
private String fileId; private String fileId;
@@ -54,6 +54,9 @@ public class LawCase {
@Column(name = "lawyer") @Column(name = "lawyer")
private String lawyer; private String lawyer;
@Column(name = "violation_id")
private Long violationId;
@PrePersist @PrePersist
public void prePersist() { public void prePersist() {
if (this.createdAt == null) { if (this.createdAt == null) {
@@ -6,8 +6,10 @@ import lombok.*;
import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener; import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@Data @Data
@@ -13,6 +13,8 @@ public enum NotificationType {
PAYMENT_RESULT("notification-payment-result"), PAYMENT_RESULT("notification-payment-result"),
REFERRAL_DEPOSIT("notification-referral-deposit"), REFERRAL_DEPOSIT("notification-referral-deposit"),
REFERRAL_REGISTERED("notification-referral-registered"), REFERRAL_REGISTERED("notification-referral-registered"),
USER_VERIFIED("notification-user-verified"),
USER_NOT_VERIFIED("notification-user-not-verified"),
REFERRAL_ACTIVATED("notification-referral-activated"), REFERRAL_ACTIVATED("notification-referral-activated"),
COMPLAINT_STATUS_CHANGED("notification-complaint-status"), COMPLAINT_STATUS_CHANGED("notification-complaint-status"),
CASE_STATUS_CHANGED("notification-case-status"), CASE_STATUS_CHANGED("notification-case-status"),
@@ -20,6 +20,7 @@ public class TariffInfo {
private String id; private String id;
@Column(name = "status", nullable = false) @Column(name = "status", nullable = false)
@Enumerated(EnumType.STRING)
private TariffStatus status; private TariffStatus status;
@Column(name = "start_tariff") @Column(name = "start_tariff")
@@ -0,0 +1,12 @@
package ru.soune.nocopy.entity.tokenoperation;
public enum OperationType {
SEARCH("search"), MONITORING("monitoring"), CREATE_LEGAL_CASE("legal_case"),
FILE_UPLOAD("file_upload"), GLOBAL_SEARCH("global_search");
private final String name;
private OperationType(String name) {
this.name = name;
}
}
@@ -0,0 +1,32 @@
package ru.soune.nocopy.entity.tokenoperation;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
@Table(name = "token_operation")
@Entity
@Setter @Getter
@NoArgsConstructor
public class TokenOperation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
@Column(name = "operation_type")
private OperationType operationType;
@Column(name = "user_id")
private Long userId;
@Column(name = "spent")
private Long spent;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
}
@@ -7,9 +7,9 @@ public enum ModerationStatus {
NOT_VERIFIED("not_verified") , VERIFICATION_IN_PROGRESS("verification_in_progress"), NOT_VERIFIED("not_verified") , VERIFICATION_IN_PROGRESS("verification_in_progress"),
VERIFIED("verified"), VERIFICATION_FAILED("verification_failed"); VERIFIED("verified"), VERIFICATION_FAILED("verification_failed");
private final String name; private final String value;
private ModerationStatus(String name) { private ModerationStatus(String value) {
this.name = name; this.value = value;
} }
} }
@@ -107,6 +107,7 @@ public class User {
private UserNotActive userNotActive; private UserNotActive userNotActive;
@Column(name = "verification_status") @Column(name = "verification_status")
@Enumerated(EnumType.STRING)
private ModerationStatus verificationStatus = ModerationStatus.NOT_VERIFIED; private ModerationStatus verificationStatus = ModerationStatus.NOT_VERIFIED;
public TariffInfo getActiveTariffInfo() { public TariffInfo getActiveTariffInfo() {
@@ -77,10 +77,9 @@ public class ComplaintEntityHandler implements RequestHandler {
private BaseResponse handleGetAll(Integer msgId, ComplaintRequest req) { private BaseResponse handleGetAll(Integer msgId, ComplaintRequest req) {
Pageable pageable = PageRequest.of( Pageable pageable = PageRequest.of(
req.getPage() != null ? req.getPage() : 0, req.getPage() != null ? req.getPage() : 0,
req.getSize() != null ? req.getSize() : 20, req.getSize() != null ? req.getSize() : 5,
Sort.by(Sort.Direction.fromString(req.getSortDirection() != null ? req.getSortDirection() : "desc"), Sort.by(Sort.Direction.fromString(req.getSortDirection() != null ? req.getSortDirection() : "desc"),
req.getSortBy() != null ? req.getSortBy() : "createdAt") req.getSortBy() != null ? req.getSortBy() : "updatedAt"));
);
var page = complaintService.getAllComplaints(pageable); var page = complaintService.getAllComplaints(pageable);
Map<String, Object> data = new HashMap<>(); Map<String, Object> data = new HashMap<>();
@@ -4,6 +4,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.*; import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.dto.file.*; import ru.soune.nocopy.dto.file.*;
@@ -15,6 +18,7 @@ import ru.soune.nocopy.entity.file.moderation.ModerationLog;
import ru.soune.nocopy.exception.*; import ru.soune.nocopy.exception.*;
import ru.soune.nocopy.repository.FileAppealRepository; import ru.soune.nocopy.repository.FileAppealRepository;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import ru.soune.nocopy.repository.ModerationLogRepository; import ru.soune.nocopy.repository.ModerationLogRepository;
import ru.soune.nocopy.service.file.moderation.ModerationService; import ru.soune.nocopy.service.file.moderation.ModerationService;
import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.register.AuthService;
@@ -40,6 +44,8 @@ public class FileEntityHandler implements RequestHandler {
private final FileEntityRepository fileEntityRepository; private final FileEntityRepository fileEntityRepository;
private final CloudStorageService cloudStorageService;
private final ModerationService moderationService; private final ModerationService moderationService;
private final FileAppealRepository fileAppealRepository; private final FileAppealRepository fileAppealRepository;
@@ -225,6 +231,52 @@ public class FileEntityHandler implements RequestHandler {
Map.of("file_status", newStatus, "file_id", fileId)); Map.of("file_status", newStatus, "file_id", fileId));
} }
// private BaseResponse handleSearchFiles(BaseRequest request, FileEntityRequest fileRequest) {
// try {
// Long userId = authService.useUserAuthToken(fileRequest.getToken());
// int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1;
// int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20;
// String fileRequestSortBy = fileRequest.getSortBy();
// String sortBy = !fileRequestSortBy.equals("") ? fileRequest.getSortBy(): "createdAt";
// sortBy = sortBy.equals("fileName") ? "originalFileName" : sortBy;
//
// String sortOrder = fileRequest.getSortOrder();
// Sort.Direction direction = (sortOrder != null && sortOrder.equalsIgnoreCase("desc"))
// ? Sort.Direction.DESC
// : Sort.Direction.ASC;
// Sort sort = Sort.by(direction, sortBy);
//
// Pageable pageable = PageRequest.of(page, pageSize, sort);
// FileResponse userFiles = fileEntityService.getUserFiles(userId, pageable, request.getVersion());
//
// FileListResponse response = FileListResponse.builder()
// .files(userFiles.getFiles())
// .totalCount(userFiles.getTotalCount())
// .totalSize(userFiles.getTotalSize())
// .formattedTotalSize(fileEntityService.formatFileSize(userFiles.getTotalSize()))
// .page(page)
// .pageSize(pageSize)
// .sortBy(sortBy)
// .sortOrder(sortOrder)
// .build();
//
// return new BaseResponse(request.getMsgId(),
// MessageCode.SUCCESS.getCode(),
// MessageCode.SUCCESS.getDescription(),
// response);
//
// } catch (NotFoundAuthToken e) {
// return new BaseResponse(request.getMsgId(),
// MessageCode.INVALID_TOKEN.getCode(),
// "Authentication required", null);
// } catch (Exception e) {
// log.error("Error searching files", e);
// return new BaseResponse(request.getMsgId(),
// MessageCode.FILE_UPLOAD_ERROR.getCode(),
// "Failed to search files: " + e.getMessage(), null);
// }
// }
private BaseResponse handleSearchFiles(BaseRequest request, FileEntityRequest fileRequest) { private BaseResponse handleSearchFiles(BaseRequest request, FileEntityRequest fileRequest) {
try { try {
Long userId = authService.useUserAuthToken(fileRequest.getToken()); Long userId = authService.useUserAuthToken(fileRequest.getToken());
@@ -289,6 +341,73 @@ public class FileEntityHandler implements RequestHandler {
} }
} }
/**
*
* private BaseResponse handleSearchFiles(BaseRequest request, FileEntityRequest fileRequest) {
* try {
* Long userId = authService.useUserAuthToken(fileRequest.getToken());
* int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1;
* int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20;
*
* String sortBy = fileRequest.getSortBy() != null ? fileRequest.getSortBy() : "fileName";
* SortOrder sortOrder = SortOrder.fromString(fileRequest.getSortOrder());
*
* FileResponse allFiles = fileEntityService.getUserFiles(userId, 1, 1000, request.getVersion());
*
* String searchQuery = fileRequest.getQuery() != null ? fileRequest.getQuery().toLowerCase().trim() : "";
* String[] searchTerms = searchQuery.split("\\s+");
* String filterType = fileRequest.getType();
* String dateFilter = fileRequest.getDateFilter();
*
* List<FileEntityResponse> filteredFiles = allFiles.getFiles().stream()
* .filter(f -> matchesSearch(f, searchTerms, searchQuery, filterType))
* .filter(f -> matchesDateFilter(f, dateFilter))
* .collect(Collectors.toList());
*
* Comparator<FileEntityResponse> comparator = getComparator(sortBy, sortOrder);
* filteredFiles.sort(comparator);
*
* int start = (page - 1) * pageSize;
* int end = Math.min(start + pageSize, filteredFiles.size());
*
* if (start >= filteredFiles.size()) {
* return createEmptyResponse(request, page, pageSize, sortBy, sortOrder.getValue());
* }
*
* List<FileEntityResponse> paginatedFiles = filteredFiles.subList(start, end);
* long totalSize = paginatedFiles.stream()
* .mapToLong(FileEntityResponse::getFileSize)
* .sum();
*
* FileListResponse response = FileListResponse.builder()
* .files(paginatedFiles)
* .totalCount(filteredFiles.size())
* .totalSize(totalSize)
* .formattedTotalSize(fileEntityService.formatFileSize(totalSize))
* .page(page)
* .pageSize(pageSize)
* .sortBy(sortBy)
* .sortOrder(sortOrder.getValue())
* .build();
*
* return new BaseResponse(request.getMsgId(),
* MessageCode.SUCCESS.getCode(),
* MessageCode.SUCCESS.getDescription(),
* response);
*
* } catch (NotFoundAuthToken e) {
* return new BaseResponse(request.getMsgId(),
* MessageCode.INVALID_TOKEN.getCode(),
* "Authentication required", null);
* } catch (Exception e) {
* log.error("Error searching files", e);
* return new BaseResponse(request.getMsgId(),
* MessageCode.FILE_UPLOAD_ERROR.getCode(),
* "Failed to search files: " + e.getMessage(), null);
* }
* }
*/
private boolean matchesDateFilter(FileEntityResponse file, String dateFilter) { private boolean matchesDateFilter(FileEntityResponse file, String dateFilter) {
if (dateFilter == null || dateFilter.isEmpty()) { if (dateFilter == null || dateFilter.isEmpty()) {
return true; return true;
@@ -420,6 +539,12 @@ public class FileEntityHandler implements RequestHandler {
Comparator.nullsLast(Comparator.naturalOrder()) Comparator.nullsLast(Comparator.naturalOrder())
); );
break; break;
case "monitoring":
comparator = Comparator.comparing(
FileEntityResponse::getMonitoring,
Comparator.nullsLast(Comparator.naturalOrder())
);
break;
default: default:
comparator = Comparator.comparing( comparator = Comparator.comparing(
@@ -578,7 +703,7 @@ public class FileEntityHandler implements RequestHandler {
int fullDelete = fileRequest.getFullDelete() == null ? 0 : fileRequest.getFullDelete(); int fullDelete = fileRequest.getFullDelete() == null ? 0 : fileRequest.getFullDelete();
if (fileEntity.getStatus().equals(FileStatus.DELETED) || fullDelete == 1) { if (fileEntity.getStatus().equals(FileStatus.DELETED) || fullDelete == 1) {
fileEntityService.deleteFromDisk(fileEntity); cloudStorageService.deleteFromStorageDisk(fileEntity);
response = DeleteFileResponse.builder() response = DeleteFileResponse.builder()
.fileId(fileRequest.getFileId()) .fileId(fileRequest.getFileId())
.message("File deleted from disk") .message("File deleted from disk")
@@ -11,6 +11,7 @@ import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.file.ImageSearchRequest; import ru.soune.nocopy.dto.file.ImageSearchRequest;
import ru.soune.nocopy.dto.file.YandexSearchResponse; import ru.soune.nocopy.dto.file.YandexSearchResponse;
import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.tokenoperation.OperationType;
import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.service.file.CheckCounterService; import ru.soune.nocopy.service.file.CheckCounterService;
@@ -117,7 +118,8 @@ public class ImageFoundRequestHandler implements RequestHandler {
log.info("Results only from Yandex: {} images", allUniqueImages.size()); log.info("Results only from Yandex: {} images", allUniqueImages.size());
} }
tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH); tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH,
OperationType.SEARCH);
checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType()); checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType());
int page = imageSearchRequest.getPage() != null ? imageSearchRequest.getPage() : 1; int page = imageSearchRequest.getPage() != null ? imageSearchRequest.getPage() : 1;
@@ -15,6 +15,7 @@ import ru.soune.nocopy.dto.complaint.PaginatedResponse;
import ru.soune.nocopy.entity.complaint.LawCase; import ru.soune.nocopy.entity.complaint.LawCase;
import ru.soune.nocopy.entity.complaint.LawCasePriority; import ru.soune.nocopy.entity.complaint.LawCasePriority;
import ru.soune.nocopy.entity.complaint.LawCaseType; import ru.soune.nocopy.entity.complaint.LawCaseType;
import ru.soune.nocopy.entity.tokenoperation.OperationType;
import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.exception.TariffNotFoundException; import ru.soune.nocopy.exception.TariffNotFoundException;
@@ -79,6 +80,7 @@ public class LawCaseHandler implements RequestHandler {
lawCaseRequest.getFilterType(), lawCaseRequest.getFilterType(),
lawCaseRequest.getFilterLawyer(), lawCaseRequest.getFilterLawyer(),
lawCaseRequest.getFilterPriority(), lawCaseRequest.getFilterPriority(),
lawCaseRequest.getViolationId(),
lawCaseRequest.getSortBy(), lawCaseRequest.getSortBy(),
lawCaseRequest.getSortDir()); lawCaseRequest.getSortDir());
@@ -93,8 +95,7 @@ public class LawCaseHandler implements RequestHandler {
lawCases.getNumber(), lawCases.getNumber(),
lawCases.getSize(), lawCases.getSize(),
lawCases.isFirst(), lawCases.isFirst(),
lawCases.isLast() lawCases.isLast());
);
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), paginatedResponse); MessageCode.SUCCESS.getDescription(), paginatedResponse);
@@ -114,7 +115,7 @@ public class LawCaseHandler implements RequestHandler {
private BaseResponse createLawCase(Long userId, LawCaseRequest lawCaseRequest, BaseRequest request) { private BaseResponse createLawCase(Long userId, LawCaseRequest lawCaseRequest, BaseRequest request) {
try { try {
tariffInfoService.writeOffTokens(userId, TariffConstants.LEGAL_COST); tariffInfoService.writeOffTokens(userId, TariffConstants.LEGAL_COST, OperationType.CREATE_LEGAL_CASE);
} catch (TariffNotFoundException e) { } catch (TariffNotFoundException e) {
return new BaseResponse(request.getMsgId(), MessageCode.USER_NOT_HAVE_TOKEN.getCode(), return new BaseResponse(request.getMsgId(), MessageCode.USER_NOT_HAVE_TOKEN.getCode(),
MessageCode.USER_NOT_HAVE_TOKEN.getDescription(), MessageCode.USER_NOT_HAVE_TOKEN.getDescription(),
@@ -132,8 +133,8 @@ public class LawCaseHandler implements RequestHandler {
lawCaseRequest.getAmount(), lawCaseRequest.getAmount(),
lawCaseRequest.getDescription(), lawCaseRequest.getDescription(),
lawCaseRequest.getName(), lawCaseRequest.getName(),
LawCasePriority.valueOf(lawCaseRequest.getPriority())); LawCasePriority.valueOf(lawCaseRequest.getPriority()),
lawCase.setUser(user); lawCaseRequest.getViolationId(), user);
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), LawCaseResponse.fromEntity(lawCase)); MessageCode.SUCCESS.getDescription(), LawCaseResponse.fromEntity(lawCase));
@@ -0,0 +1,30 @@
package ru.soune.nocopy.handler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.statistic.IncomeStatisticResponse;
import ru.soune.nocopy.service.statistic.IncomeStatisticService;
@Component
@RequiredArgsConstructor
@Slf4j
public class StatisticIncomeHandler implements RequestHandler {
private final IncomeStatisticService statisticService;
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
IncomeStatisticResponse statistics = statisticService.getIncomeStatistics();
return new BaseResponse(
request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
statistics
);
}
}
@@ -0,0 +1,28 @@
package ru.soune.nocopy.handler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.statistic.ProtectedFilesStatisticResponse;
import ru.soune.nocopy.service.statistic.ProtectedFilesStatisticService;
@Component
@RequiredArgsConstructor
@Slf4j
public class StatisticProtectedFilesHandler implements RequestHandler {
private final ProtectedFilesStatisticService statisticService;
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
ProtectedFilesStatisticResponse statistics = statisticService.getProtectedFilesStatistics();
return new BaseResponse(
request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
statistics);
}
}
@@ -0,0 +1,29 @@
package ru.soune.nocopy.handler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.statistic.SubscriberStatisticResponse;
import ru.soune.nocopy.service.statistic.SubscriberStatisticService;
@Component
@RequiredArgsConstructor
@Slf4j
public class StatisticSubscriberHandler implements RequestHandler {
private final SubscriberStatisticService statisticService;
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
SubscriberStatisticResponse statistics = statisticService.getSubscriberStatistics();
return new BaseResponse(
request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
statistics
);
}
}
@@ -0,0 +1,48 @@
package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.statistic.TariffStatisticFullResponse;
import ru.soune.nocopy.dto.statistic.TariffStatisticRequest;
import ru.soune.nocopy.dto.statistic.TariffStatisticResponse;
import ru.soune.nocopy.repository.TariffInfoRepository;
import java.util.List;
@Component
@AllArgsConstructor
public class StatisticTariffInfoFileHandler implements RequestHandler {
private ObjectMapper objectMapper;
private TariffInfoRepository tariffInfoRepository;
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
TariffStatisticRequest tariffRequest =
objectMapper.convertValue(request.getMessageBody(), TariffStatisticRequest.class);
boolean activeOnly = tariffRequest.getActiveOnly() != null ? tariffRequest.getActiveOnly() : true;
List<Object[]> results = tariffInfoRepository.getTariffStatisticsNative(activeOnly);
List<TariffStatisticResponse> tariffs = results.stream()
.map(row -> new TariffStatisticResponse(
(String) row[0],
((Number) row[1]).longValue(),
((Number) row[2]).doubleValue()
))
.toList();
TariffStatisticFullResponse statistics = new TariffStatisticFullResponse(tariffs);
return new BaseResponse(
request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
statistics);
}
}
@@ -0,0 +1,29 @@
package ru.soune.nocopy.handler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.statistic.TokenStatisticResponse;
import ru.soune.nocopy.service.statistic.TokenStatisticService;
@Component
@RequiredArgsConstructor
@Slf4j
public class StatisticTokenHandler implements RequestHandler {
private final TokenStatisticService statisticService;
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
TokenStatisticResponse statistics = statisticService.getTokenStatistics();
return new BaseResponse(
request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
statistics
);
}
}
@@ -0,0 +1,29 @@
package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.statistic.UserDynamicStatisticResponse;
import ru.soune.nocopy.service.statistic.UserDynamicStatisticService;
@Component
@RequiredArgsConstructor
@Slf4j
public class StatisticUserDynamicHandler implements RequestHandler {
private final UserDynamicStatisticService statisticService;
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
UserDynamicStatisticResponse statistics = statisticService.getUserDynamicStatistics();
return new BaseResponse(
request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
statistics);
}
}
@@ -0,0 +1,38 @@
package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.statistic.UserFilesStatisticRequest;
import ru.soune.nocopy.dto.statistic.UserFilesStatisticResponse;
import ru.soune.nocopy.repository.FileEntityRepository;
import java.util.List;
@Component
@RequiredArgsConstructor
@Slf4j
public class StatisticUserFilesHandler implements RequestHandler {
private final ObjectMapper objectMapper;
private final FileEntityRepository fileRepository;
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
UserFilesStatisticRequest userFilesStatisticRequest =
objectMapper.convertValue(request.getMessageBody(), UserFilesStatisticRequest.class);
int topUsers = userFilesStatisticRequest.getTopUsers();
String fileMimeType = userFilesStatisticRequest.getFileMimeType();
List<UserFilesStatisticResponse> files = fileRepository.findTopUsers(topUsers, fileMimeType);
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), files);
}
}
@@ -0,0 +1,34 @@
package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.statistic.ViolationStatisticResponse;
import ru.soune.nocopy.service.statistic.ViolationStatisticService;
@Component
@RequiredArgsConstructor
@Slf4j
public class StatisticViolationHandler implements RequestHandler {
private final ObjectMapper objectMapper;
private final ViolationStatisticService statisticService;
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
log.debug("Handling violation statistic request, msgId: {}", request.getMsgId());
ViolationStatisticResponse statistics = statisticService.getViolationStatistics();
return new BaseResponse(
request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
statistics
);
}
}
@@ -0,0 +1,472 @@
package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.tokenoperation.TokenOperationPageDto;
import ru.soune.nocopy.dto.tokenoperation.TokenOperationRequest;
import ru.soune.nocopy.entity.tokenoperation.OperationType;
import ru.soune.nocopy.entity.tokenoperation.TokenOperation;
import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.tokenoperation.TokenOperationService;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
@RequiredArgsConstructor
public class TokenOperationHandler implements RequestHandler {
private final TokenOperationService tokenOperationService;
private final ObjectMapper objectMapper;
private final AuthService authService;
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
log.info("Handling TokenOperation request: msg_id={}, version={}",
request.getMsgId(), request.getVersion());
try {
TokenOperationRequest tokenRequest = objectMapper.convertValue(
request.getMessageBody(),
TokenOperationRequest.class
);
String action = tokenRequest.getAction();
if (action == null) {
return new BaseResponse(
request.getMsgId(),
MessageCode.INVALID_ACTION.getCode(),
MessageCode.INVALID_ACTION.getDescription(),
Map.of("error", "Action is required")
);
}
switch (action.toLowerCase()) {
case "get_list":
return handleGetList(request.getMsgId(), tokenRequest);
case "get_by_id":
return handleGetById(request.getMsgId(), tokenRequest);
case "get_by_user":
return handleGetByUser(request.getMsgId(), tokenRequest);
case "create":
return handleCreate(request.getMsgId(), tokenRequest);
case "update":
return handleUpdate(request.getMsgId(), tokenRequest);
case "delete":
return handleDelete(request.getMsgId(), tokenRequest);
case "delete_all_by_user":
return handleDeleteAllByUser(request.getMsgId(), tokenRequest);
case "get_stats":
return handleGetStats(request.getMsgId(), tokenRequest);
case "filter":
return handleFilter(request.getMsgId(), tokenRequest);
default:
return new BaseResponse(
request.getMsgId(),
MessageCode.INVALID_ACTION.getCode(),
MessageCode.INVALID_ACTION.getDescription(),
Map.of("available_actions",
"get_list, get_by_id, get_by_user, create, update, " +
"delete, delete_all_by_user, get_stats, filter")
);
}
} catch (Exception e) {
log.error("Error handling TokenOperation request", e);
return new BaseResponse(
request.getMsgId(),
MessageCode.INTERNAL_ERROR.getCode(),
MessageCode.INTERNAL_ERROR.getDescription(),
Map.of("error", e.getMessage())
);
}
}
private BaseResponse handleGetList(Integer msgId, TokenOperationRequest request) {
if (request.getToken() == null) {
return new BaseResponse(
msgId,
MessageCode.USER_NOT_FOUND.getCode(),
MessageCode.USER_NOT_FOUND.getDescription(),
Map.of("error", "User ID is required")
);
}
Long userId = authService.useUserAuthToken(request.getToken());
try {
Page<TokenOperation> page = tokenOperationService.findByUserId(
userId,
request.getPage() != null ? request.getPage() : 0,
request.getSize() != null ? request.getSize() : 20
);
TokenOperationPageDto pageDto = TokenOperationPageDto.builder()
.content(page.getContent())
.pageNumber(page.getNumber())
.pageSize(page.getSize())
.totalElements(page.getTotalElements())
.totalPages(page.getTotalPages())
.last(page.isLast())
.first(page.isFirst())
.build();
Map<String, Object> responseData = new HashMap<>();
responseData.put("operations", pageDto);
responseData.put("total_elements", page.getTotalElements());
responseData.put("total_pages", page.getTotalPages());
responseData.put("current_page", page.getNumber());
responseData.put("page_size", page.getSize());
return new BaseResponse(
msgId,
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
responseData
);
} catch (Exception e) {
log.error("Error getting token operations list", e);
return new BaseResponse(
msgId,
MessageCode.INTERNAL_ERROR.getCode(),
MessageCode.INTERNAL_ERROR.getDescription(),
Map.of("error", "Error getting operations: " + e.getMessage())
);
}
}
private BaseResponse handleGetById(Integer msgId, TokenOperationRequest request) {
log.info("Getting token operation by id: {}", request.getOperationId());
if (request.getOperationId() == null) {
return new BaseResponse(
msgId,
MessageCode.OPERATION_NOT_FOUND.getCode(),
MessageCode.OPERATION_NOT_FOUND.getDescription(),
Map.of("error", "Operation ID is required")
);
}
try {
TokenOperation operation = tokenOperationService.findById(request.getOperationId());
return new BaseResponse(
msgId,
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
operation
);
} catch (jakarta.persistence.EntityNotFoundException e) {
return new BaseResponse(
msgId,
MessageCode.NOT_FOUND.getCode(),
MessageCode.NOT_FOUND.getDescription(),
Map.of("error", "Operation not found with id: " + request.getOperationId())
);
} catch (Exception e) {
log.error("Error getting token operation by id", e);
return new BaseResponse(
msgId,
MessageCode.INTERNAL_ERROR.getCode(),
MessageCode.INTERNAL_ERROR.getDescription(),
Map.of("error", e.getMessage())
);
}
}
private BaseResponse handleGetByUser(Integer msgId, TokenOperationRequest request) {
return handleGetList(msgId, request);
}
private BaseResponse handleCreate(Integer msgId, TokenOperationRequest request) {
if (request.getToken() == null) {
return new BaseResponse(
msgId,
MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(),
MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(),
Map.of("error", "User ID is required")
);
}
Long userId = authService.useUserAuthToken(request.getToken());
try {
TokenOperation operation = tokenOperationService.create(
request.getOperationType(),
userId,
request.getSpent()
);
return new BaseResponse(
msgId,
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
operation
);
} catch (Exception e) {
log.error("Error creating token operation", e);
return new BaseResponse(
msgId,
MessageCode.INTERNAL_ERROR.getCode(),
MessageCode.INTERNAL_ERROR.getDescription(),
Map.of("error", "Error creating operation: " + e.getMessage())
);
}
}
private BaseResponse handleUpdate(Integer msgId, TokenOperationRequest request) {
log.info("Updating token operation: {}", request.getOperationId());
if (request.getOperationId() == null) {
return new BaseResponse(
msgId,
MessageCode.OPERATION_NOT_FOUND.getCode(),
MessageCode.OPERATION_NOT_FOUND.getDescription(),
Map.of("error", "Operation ID is required")
);
}
try {
TokenOperation existing = tokenOperationService.findById(request.getOperationId());
if (request.getOperationType() != null) {
existing.setOperationType(request.getOperationType());
}
if (request.getSpent() != null) {
existing.setSpent(request.getSpent());
}
TokenOperation updated = tokenOperationService.update(request.getOperationId(), existing);
return new BaseResponse(
msgId,
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
updated
);
} catch (jakarta.persistence.EntityNotFoundException e) {
return new BaseResponse(
msgId,
MessageCode.NOT_FOUND.getCode(),
MessageCode.NOT_FOUND.getDescription(),
Map.of("error", "Operation not found with id: " + request.getOperationId())
);
} catch (Exception e) {
log.error("Error updating token operation", e);
return new BaseResponse(
msgId,
MessageCode.INTERNAL_ERROR.getCode(),
MessageCode.INTERNAL_ERROR.getDescription(),
Map.of("error", "Error updating operation: " + e.getMessage())
);
}
}
private BaseResponse handleDelete(Integer msgId, TokenOperationRequest request) {
log.info("Deleting token operation: {}", request.getOperationId());
if (request.getOperationId() == null) {
return new BaseResponse(
msgId,
MessageCode.OPERATION_NOT_FOUND.getCode(),
MessageCode.OPERATION_NOT_FOUND.getDescription(),
Map.of("error", "Operation ID is required")
);
}
try {
tokenOperationService.deleteById(request.getOperationId());
return new BaseResponse(
msgId,
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
Map.of("message", "Operation deleted successfully",
"operation_id", request.getOperationId())
);
} catch (jakarta.persistence.EntityNotFoundException e) {
return new BaseResponse(
msgId,
MessageCode.NOT_FOUND.getCode(),
MessageCode.NOT_FOUND.getDescription(),
Map.of("error", "Operation not found with id: " + request.getOperationId())
);
} catch (Exception e) {
log.error("Error deleting token operation", e);
return new BaseResponse(
msgId,
MessageCode.INTERNAL_ERROR.getCode(),
MessageCode.INTERNAL_ERROR.getDescription(),
Map.of("error", "Error deleting operation: " + e.getMessage())
);
}
}
private BaseResponse handleDeleteAllByUser(Integer msgId, TokenOperationRequest request) {
if (request.getToken() == null) {
return new BaseResponse(
msgId,
MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(),
MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(),
Map.of("error", "User ID is required")
);
}
Long userId = authService.useUserAuthToken(request.getToken());
try {
tokenOperationService.deleteByUserId(userId);
return new BaseResponse(
msgId,
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
Map.of("message", "All operations deleted successfully for user",
"user_id", userId)
);
} catch (Exception e) {
log.error("Error deleting all operations for user", e);
return new BaseResponse(
msgId,
MessageCode.INTERNAL_ERROR.getCode(),
MessageCode.INTERNAL_ERROR.getDescription(),
Map.of("error", "Error deleting operations: " + e.getMessage())
);
}
}
private BaseResponse handleGetStats(Integer msgId, TokenOperationRequest request) {
if (request.getToken() == null) {
return new BaseResponse(
msgId,
MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(),
MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(),
Map.of("error", "User ID is required")
);
}
Long userId = authService.useUserAuthToken(request.getToken());
if (userId == null) {
return new BaseResponse(
msgId,
MessageCode.USER_NOT_FOUND.getCode(),
MessageCode.USER_NOT_FOUND.getDescription(),
Map.of("error", "User ID is required")
);
}
try {
Map<String, Long> statsByType = new HashMap<>();
long totalSpent = 0L;
for (OperationType type : OperationType.values()) {
Long spent = tokenOperationService.getTotalSpentByUserAndType(userId, type);
statsByType.put(type.name().toLowerCase(), spent != null ? spent : 0L);
totalSpent += spent != null ? spent : 0L;
}
Map<String, Object> statistics = new HashMap<>();
statistics.put("user_id", userId);
statistics.put("total_spent", totalSpent);
statistics.put("total_operations", tokenOperationService.countByUserId(userId));
statistics.put("stats_by_type", statsByType);
return new BaseResponse(
msgId,
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
statistics
);
} catch (Exception e) {
log.error("Error getting statistics", e);
return new BaseResponse(
msgId,
MessageCode.INTERNAL_ERROR.getCode(),
MessageCode.INTERNAL_ERROR.getDescription(),
Map.of("error", "Error getting statistics: " + e.getMessage())
);
}
}
private BaseResponse handleFilter(Integer msgId, TokenOperationRequest request) {
if (request.getToken() == null) {
return new BaseResponse(
msgId,
MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(),
MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(),
Map.of("error", "User ID is required")
);
}
Long userId = authService.useUserAuthToken(request.getToken());
try {
Page<TokenOperation> page = tokenOperationService.findByFilters(
userId,
request.getOperationType(),
request.getMinSpent(),
request.getMaxSpent(),
request.getPage() != null ? request.getPage() : 0,
request.getSize() != null ? request.getSize() : 20,
request.getSortBy() != null ? request.getSortBy() : "createdAt",
request.getSortDirection() != null ? request.getSortDirection() : "desc"
);
TokenOperationPageDto pageDto = TokenOperationPageDto.builder()
.content(page.getContent())
.pageNumber(page.getNumber())
.pageSize(page.getSize())
.totalElements(page.getTotalElements())
.totalPages(page.getTotalPages())
.last(page.isLast())
.first(page.isFirst())
.build();
Map<String, Object> responseData = new HashMap<>();
responseData.put("operations", pageDto);
responseData.put("total_elements", page.getTotalElements());
responseData.put("total_pages", page.getTotalPages());
responseData.put("current_page", page.getNumber());
responseData.put("page_size", page.getSize());
responseData.put("applied_filters", Map.of(
"user_id",userId != null ? userId : "all",
"operation_type", request.getOperationType() != null ? request.getOperationType() : "all",
"min_spent", request.getMinSpent() != null ? request.getMinSpent() : "none",
"max_spent", request.getMaxSpent() != null ? request.getMaxSpent() : "none"
));
return new BaseResponse(
msgId,
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
responseData
);
} catch (Exception e) {
log.error("Error filtering operations", e);
return new BaseResponse(
msgId,
MessageCode.INTERNAL_ERROR.getCode(),
MessageCode.INTERNAL_ERROR.getDescription(),
Map.of("error", "Error filtering operations: " + e.getMessage())
);
}
}
}
@@ -10,7 +10,10 @@ import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.file.ActionResponse; import ru.soune.nocopy.dto.file.ActionResponse;
import ru.soune.nocopy.dto.user.moderation.UserVerificationRequest; import ru.soune.nocopy.dto.user.moderation.UserVerificationRequest;
import ru.soune.nocopy.dto.user.moderation.UserVerificationResponse; import ru.soune.nocopy.dto.user.moderation.UserVerificationResponse;
import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.entity.user.ModerationStatus;
import ru.soune.nocopy.entity.user.moderation.UserVerification; import ru.soune.nocopy.entity.user.moderation.UserVerification;
import ru.soune.nocopy.service.notification.NotificationService;
import ru.soune.nocopy.service.user.moderation.UserVerificationService; import ru.soune.nocopy.service.user.moderation.UserVerificationService;
import java.util.Arrays; import java.util.Arrays;
@@ -25,6 +28,8 @@ public class UserVerificationHandler implements RequestHandler {
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final NotificationService notificationService;
@Override @Override
public BaseResponse handle(BaseRequest request) throws Exception { public BaseResponse handle(BaseRequest request) throws Exception {
UserVerificationRequest userVerificationRequest = objectMapper.convertValue(request.getMessageBody(), UserVerificationRequest userVerificationRequest = objectMapper.convertValue(request.getMessageBody(),
@@ -76,6 +81,11 @@ public class UserVerificationHandler implements RequestHandler {
null); null);
} }
NotificationType notificationType = userVerification.getModerationStatus() == ModerationStatus.VERIFIED?
NotificationType.USER_VERIFIED: NotificationType.USER_NOT_VERIFIED;
notificationService.addNotification(notificationType, userVerification.getUserId());
return BaseResponse.builder() return BaseResponse.builder()
.msgId(request.getMsgId()) .msgId(request.getMsgId())
.messageBody(UserVerificationResponse.builder() .messageBody(UserVerificationResponse.builder()
@@ -13,6 +13,7 @@ import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.violation.Violation; import ru.soune.nocopy.entity.violation.Violation;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.geo.GeoCountryService;
import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.violation.ViolationService; import ru.soune.nocopy.service.violation.ViolationService;
import ru.soune.nocopy.service.violation.ViolationStatus; import ru.soune.nocopy.service.violation.ViolationStatus;
@@ -21,6 +22,7 @@ import java.io.FileNotFoundException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException; import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -38,6 +40,8 @@ public class ViolationHandler implements RequestHandler {
private final FileEntityService fileEntityService; private final FileEntityService fileEntityService;
private final GeoCountryService geoCountryService;
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override @Override
@@ -112,10 +116,16 @@ public class ViolationHandler implements RequestHandler {
); );
} }
List<ViolationResponse.ViolationDto> violationDtos = violationPage.getContent() List<Violation> content = violationPage.getContent();
.stream() List<ViolationResponse.ViolationDto> violationDtos = new ArrayList<>();
.map(ViolationResponse.ViolationDto::fromEntity)
.toList(); for (Violation violation : content) {
ViolationResponse.ViolationDto violationDto = ViolationResponse.ViolationDto.fromEntity(violation);
violationDto.setCountry(geoCountryService.getCountryName(violation.getPageUrl()));
violationDto.setCountryCode(geoCountryService.getCountryCode(violation.getPageUrl()));
violationDtos.add(violationDto);
}
ViolationResponse response = ViolationResponse.builder() ViolationResponse response = ViolationResponse.builder()
.violations(violationDtos) .violations(violationDtos)
@@ -25,4 +25,16 @@ public interface ComplaintEntityRepository extends JpaRepository<ComplaintEntity
@Transactional @Transactional
@Query("UPDATE ComplaintEntity c SET c.status = :status WHERE c.id = :id") @Query("UPDATE ComplaintEntity c SET c.status = :status WHERE c.id = :id")
int updateStatus(Long id, ComplaintStatus status); int updateStatus(Long id, ComplaintStatus status);
@Query("SELECT COUNT(c.id) FROM ComplaintEntity c")
Long getTotalComplaintsCount();
@Query("""
SELECT COUNT(c.id)
FROM ComplaintEntity c
WHERE NOT EXISTS (
SELECT 1 FROM LawCase lc WHERE lc.violationId = c.violation.id
)
""")
Long getComplaintsWithoutLawCaseCount();
} }
@@ -6,10 +6,12 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import ru.soune.nocopy.dto.statistic.UserFilesStatisticResponse;
import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.ProtectionStatus; import ru.soune.nocopy.entity.file.ProtectionStatus;
import java.time.LocalDateTime;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -58,12 +60,6 @@ public interface FileEntityRepository extends JpaRepository<FileEntity, String>
List<FileEntity> findFileByUserIdAndStatus(Long userId, FileStatus status); List<FileEntity> findFileByUserIdAndStatus(Long userId, FileStatus status);
@Query("SELECT f FROM FileEntity f WHERE f.userId = :userId AND f.mimeType = :mimeType")
List<FileEntity> findByUserIdAndMimeType(Long userId, String mimeType);
@Query("SELECT f FROM FileEntity f WHERE f.mimeType = :mimeType")
List<FileEntity> findByMimeType(String mimeType);
@Query("SELECT f FROM FileEntity f WHERE f.mimeType = :mimeType AND f.userId IN :userIds") @Query("SELECT f FROM FileEntity f WHERE f.mimeType = :mimeType AND f.userId IN :userIds")
List<FileEntity> findByMimeTypeAndUserIds(String mimeType, List<Long> userIds); List<FileEntity> findByMimeTypeAndUserIds(String mimeType, List<Long> userIds);
@@ -78,4 +74,61 @@ public interface FileEntityRepository extends JpaRepository<FileEntity, String>
@Query("SELECT f FROM FileEntity f WHERE f.userId = :userId AND f.status IN :statuses") @Query("SELECT f FROM FileEntity f WHERE f.userId = :userId AND f.status IN :statuses")
List<FileEntity> findByStatuses(Long userId, @Param("statuses") List<String> statuses); List<FileEntity> findByStatuses(Long userId, @Param("statuses") List<String> statuses);
@Query("SELECT DISTINCT f FROM FileEntity f " +
"LEFT JOIN FileMonitoringEntity m ON f.id = m.file.id " +
"WHERE f.userId IN :userIds " +
"AND f.status IN :statuses")
Page<FileEntity> findFiles(
@Param("userIds") List<Long> userIds,
@Param("statuses") List<FileStatus> statuses,
Pageable pageable);
@Query("SELECT SUM(f.fileSize) FROM FileEntity f " +
"WHERE f.userId IN :userIds " +
"AND f.status IN :statuses")
Long sumFileSize(
@Param("userIds") List<Long> userIds,
@Param("statuses") List<FileStatus> statuses);
@Query("""
SELECT new ru.soune.nocopy.dto.statistic.UserFilesStatisticResponse(
u.fullName,
u.Id,
CAST(COUNT(f.id) AS int),
COALESCE(SUM(f.fileSize), 0L)
)
FROM FileEntity f
JOIN User u ON f.userId = u.Id
WHERE
(:mimeType IS NULL OR :mimeType = '' OR f.mimeType LIKE CONCAT(:mimeType, '%'))
GROUP BY u.Id, u.fullName
ORDER BY COUNT(f.id) DESC
LIMIT :topUsers
""")
List<UserFilesStatisticResponse> findTopUsers(
@Param("topUsers") int topUsers,
@Param("mimeType") String mimeType
);
@Query("""
SELECT
COUNT(f.id),
COALESCE(SUM(f.fileSize), 0)
FROM FileEntity f
WHERE f.protectionStatus = 'PROTECTED'
""")
List<Object[]> getProtectedFilesTotalStats();
@Query(value = """
SELECT
user_id,
COUNT(*) as file_count,
SUM(file_size) as total_size
FROM file_entities
WHERE protection_status = 'PROTECTED'
GROUP BY user_id
ORDER BY file_count
""", nativeQuery = true)
List<Object[]> getProtectedFilesPerUserStats();
} }
@@ -56,7 +56,7 @@ public interface ImageSimilarityRepository
JOIN file_entities f JOIN file_entities f
ON f.id = h.file_id ON f.id = h.file_id
WHERE ref.file_id = :fileId AND f.user_id = :userId WHERE ref.file_id = :fileId AND f.user_id = :userId
AND f.status = 'ACTIVE' AND (f.status = 'ACTIVE' OR f.status = 'MODERATION')
""", """,
nativeQuery = true) nativeQuery = true)
List<SimilarImageProjection> findCandidatesFromUserFiles( List<SimilarImageProjection> findCandidatesFromUserFiles(
@@ -82,7 +82,7 @@ public interface ImageSimilarityRepository
JOIN file_entities f JOIN file_entities f
ON f.id = h.file_id ON f.id = h.file_id
WHERE ref.file_id = :fileId AND f.user_id IN :userIds WHERE ref.file_id = :fileId AND f.user_id IN :userIds
AND f.status = 'ACTIVE' AND (f.status = 'ACTIVE' OR f.status = 'MODERATION')
""", """,
nativeQuery = true) nativeQuery = true)
List<SimilarImageProjection> findCandidatesFromUserFiles( List<SimilarImageProjection> findCandidatesFromUserFiles(
@@ -108,7 +108,7 @@ public interface ImageSimilarityRepository
JOIN file_entities f ON f.id = h.file_id JOIN file_entities f ON f.id = h.file_id
WHERE h.hash64_hi = :hash64Hi WHERE h.hash64_hi = :hash64Hi
AND h.hash64_lo = :hash64Lo AND h.hash64_lo = :hash64Lo
AND f.status = 'ACTIVE' AND (f.status = 'ACTIVE' OR f.status = 'MODERATION')
""", """,
nativeQuery = true) nativeQuery = true)
List<SimilarImageProjection> findExactDuplicates( List<SimilarImageProjection> findExactDuplicates(
@@ -31,10 +31,15 @@ public interface LawCaseRepository extends JpaRepository<LawCase, Long> {
@Query("SELECT l FROM LawCase l WHERE l.user.Id = :userId " + @Query("SELECT l FROM LawCase l WHERE l.user.Id = :userId " +
"AND (:type IS NULL OR l.type = :type) " + "AND (:type IS NULL OR l.type = :type) " +
"AND (:lawyer IS NULL OR l.lawyer = :lawyer) " + "AND (:lawyer IS NULL OR l.lawyer = :lawyer) " +
"AND (:violationId IS NULL OR l.violationId = :violationId) " +
"AND (:priority IS NULL OR l.priority = :priority)") "AND (:priority IS NULL OR l.priority = :priority)")
Page<LawCase> getUserLawCases(@Param("userId") Long userId, Page<LawCase> getUserLawCases(@Param("userId") Long userId,
@Param("type") LawCaseType type, @Param("type") LawCaseType type,
@Param("lawyer") String lawyer, @Param("lawyer") String lawyer,
@Param("priority") LawCasePriority priority, @Param("priority") LawCasePriority priority,
@Param("violationId") Long violationId,
Pageable pageable); Pageable pageable);
@Query("SELECT COUNT(lc.id) FROM LawCase lc")
Long getTotalLawCasesCount();
} }
@@ -1,6 +1,7 @@
package ru.soune.nocopy.repository; package ru.soune.nocopy.repository;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import ru.soune.nocopy.entity.payment.Payment; import ru.soune.nocopy.entity.payment.Payment;
@@ -11,4 +12,23 @@ import java.util.Optional;
public interface PaymentRepository extends JpaRepository<Payment, String> { public interface PaymentRepository extends JpaRepository<Payment, String> {
Optional<Payment> findByPaymentUuid(String paymentUuid); Optional<Payment> findByPaymentUuid(String paymentUuid);
List<Payment> findByUserId(Long userId); List<Payment> findByUserId(Long userId);
@Query("""
SELECT COALESCE(SUM(p.tariff.tokens), 0)
FROM Payment p
WHERE p.operationType = 'TOKEN'
AND p.status = 'SUCCEEDED'
""")
Long getTotalTokensBought();
@Query("""
SELECT
p.user.Id,
COALESCE(SUM(p.tariff.tokens), 0)
FROM Payment p
WHERE p.operationType = 'TOKEN'
AND p.status = 'SUCCEEDED'
GROUP BY p.user.Id
""")
List<Object[]> getTokensBoughtPerUser();
} }
@@ -43,4 +43,22 @@ public interface ReferralJpaRepository extends JpaRepository<Referral, Long> {
@Query("SELECT COUNT(r) FROM Referral r WHERE r.inviterId = :userId") @Query("SELECT COUNT(r) FROM Referral r WHERE r.inviterId = :userId")
int countTotalInvitees(@Param("userId") Long userId); int countTotalInvitees(@Param("userId") Long userId);
@Query("SELECT COALESCE(SUM(r.totalIncome), 0) FROM Referral r")
Long getTotalIncome();
@Query("SELECT COALESCE(SUM(r.availableIncome), 0) FROM Referral r")
Long getTotalAvailableIncome();
@Query("SELECT COALESCE(SUM(r.holdBalance), 0) FROM Referral r")
Long getTotalHoldBalance();
@Query("""
SELECT
r.userId,
r.totalIncome
FROM Referral r
WHERE r.totalIncome > 0
""")
List<Object[]> getIncomePerUser();
} }
@@ -5,6 +5,7 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import ru.soune.nocopy.entity.tarif.TariffInfo; import ru.soune.nocopy.entity.tarif.TariffInfo;
import ru.soune.nocopy.entity.tarif.TariffTimeTerm;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
@@ -13,4 +14,57 @@ import java.util.List;
public interface TariffInfoRepository extends JpaRepository<TariffInfo, String> { public interface TariffInfoRepository extends JpaRepository<TariffInfo, String> {
@Query("SELECT t FROM TariffInfo t WHERE t.autoRenewal = true AND t.endTariff <= :date") @Query("SELECT t FROM TariffInfo t WHERE t.autoRenewal = true AND t.endTariff <= :date")
List<TariffInfo> findForAutoRenewal(@Param("date") LocalDateTime date); List<TariffInfo> findForAutoRenewal(@Param("date") LocalDateTime date);
@Query("""
SELECT COUNT(u.Id)
FROM User u
WHERE (:activeOnly = false OR u.isActive = true)
AND (:companyOnly = false OR u.company IS NOT NULL)
""")
Long getTotalUsersCount(
@Param("activeOnly") boolean activeOnly,
@Param("companyOnly") boolean companyOnly
);
@Query("""
SELECT COUNT(u.Id)
FROM User u
WHERE (:activeOnly = false OR u.isActive = true)
""")
Long getTotalUsersCount(@Param("activeOnly") boolean activeOnly);
@Query(value = """
SELECT
t.tariff_name,
COUNT(u.id) as user_count,
(COUNT(u.id) * 100.0) / NULLIF(
(SELECT COUNT(*) FROM users u2
WHERE (:activeOnly = false OR u2.is_active = true)
AND u2.personal_tariff_info_id IS NOT NULL
), 0
) as usage_percent
FROM users u
INNER JOIN tariff_info pti ON u.personal_tariff_info_id = pti.id
INNER JOIN tariff t ON pti.tariff_id = t.id
WHERE (:activeOnly = false OR u.is_active = true)
GROUP BY t.id, t.tariff_name
ORDER BY user_count DESC
""", nativeQuery = true)
List<Object[]> getTariffStatisticsNative(@Param("activeOnly") boolean activeOnly);
@Query("""
SELECT COUNT(ti.id)
FROM TariffInfo ti
WHERE ti.endTariff >= CURRENT_TIMESTAMP
AND (:term IS NULL OR ti.tariff.tariffTerm = :term)
""")
Long getCurrentActiveSubscriptions(@Param("term") TariffTimeTerm term);
@Query("""
SELECT ti
FROM TariffInfo ti
WHERE (:term IS NULL OR ti.tariff.tariffTerm = :term)
ORDER BY ti.startTariff
""")
List<TariffInfo> getAllTariffInfosForAnalysis(@Param("term") TariffTimeTerm term);
} }
@@ -0,0 +1,56 @@
package ru.soune.nocopy.repository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import ru.soune.nocopy.entity.tokenoperation.OperationType;
import ru.soune.nocopy.entity.tokenoperation.TokenOperation;
import java.util.List;
@Repository
public interface TokenOperationRepository extends JpaRepository<TokenOperation, Long> {
Page<TokenOperation> findAll(Pageable pageable);
List<TokenOperation> findByUserId(Long userId);
Page<TokenOperation> findByUserId(Long userId, Pageable pageable);
Page<TokenOperation> findByOperationType(OperationType operationType, Pageable pageable);
@Query("SELECT t FROM TokenOperation t WHERE " +
"(:userId IS NULL OR t.userId = :userId) AND " +
"(:operationType IS NULL OR t.operationType = :operationType) AND " +
"(:minSpent IS NULL OR t.spent >= :minSpent) AND " +
"(:maxSpent IS NULL OR t.spent <= :maxSpent)")
Page<TokenOperation> findByFilters(
@Param("userId") Long userId,
@Param("operationType") OperationType operationType,
@Param("minSpent") Long minSpent,
@Param("maxSpent") Long maxSpent,
Pageable pageable
);
@Query("SELECT SUM(t.spent) FROM TokenOperation t WHERE t.userId = :userId AND t.operationType = :operationType")
Long sumSpentByUserAndType(@Param("userId") Long userId, @Param("operationType") OperationType operationType);
Long countByUserId(Long userId);
@Query("""
SELECT COALESCE(SUM(t.spent), 0)
FROM TokenOperation t
""")
Long getTotalTokensSpent();
@Query("""
SELECT
t.userId,
COALESCE(SUM(t.spent), 0)
FROM TokenOperation t
GROUP BY t.userId
""")
List<Object[]> getTokensSpentPerUser();
}
@@ -2,10 +2,13 @@ package ru.soune.nocopy.repository;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import ru.soune.nocopy.entity.tarif.TariffInfo; import ru.soune.nocopy.entity.tarif.TariffInfo;
import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.entity.user.User;
import java.util.List; import java.util.List;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> { public interface UserRepository extends JpaRepository<User, Long> {
User findByEmail(String email); User findByEmail(String email);
@@ -14,4 +17,32 @@ public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByCompanyId(String companyId); List<User> findByCompanyId(String companyId);
long countByCompanyId(String companyId); long countByCompanyId(String companyId);
User findByPersonalTariffInfo(TariffInfo tariffInfo); User findByPersonalTariffInfo(TariffInfo tariffInfo);
@Query("SELECT COUNT(u.Id) FROM User u")
Long getTotalUsersCount();
@Query(value = """
SELECT COUNT(*)
FROM users
WHERE created_at >= NOW() - INTERVAL '1 day' * :days
""", nativeQuery = true)
Long getNewUsersCount(int days);
@Query(value = """
SELECT COUNT(*)
FROM users
WHERE created_at >= DATE_TRUNC('month', NOW())
""", nativeQuery = true)
Long getNewUsersCurrentMonth();
@Query(value = """
SELECT COUNT(*)
FROM users
WHERE created_at >= DATE_TRUNC('month', NOW() - INTERVAL '1 month')
AND created_at < DATE_TRUNC('month', NOW())
""", nativeQuery = true)
Long getNewUsersPreviousMonth();
@Query("SELECT u FROM User u WHERE u.personalTariffInfo.id = :tariffInfoId")
Optional<User> findByPersonalTariffInfoId(@Param("tariffInfoId") String tariffInfoId);
} }
@@ -74,4 +74,10 @@ public interface ViolationRepository extends JpaRepository<Violation, Long> {
List<Violation> findByFileEntityInAndStatusAndCreatedDateBetween(List<FileEntity> files, String status, List<Violation> findByFileEntityInAndStatusAndCreatedDateBetween(List<FileEntity> files, String status,
LocalDateTime startDate, LocalDateTime endDate, LocalDateTime startDate, LocalDateTime endDate,
Sort sort); Sort sort);
@Query("SELECT COUNT(v.id) FROM Violation v")
Long getTotalViolationsCount();
@Query("SELECT COUNT(DISTINCT c.violation.id) FROM ComplaintEntity c WHERE c.violation IS NOT NULL")
Long getViolationsWithComplaintCount();
} }
@@ -17,6 +17,7 @@ import ru.soune.nocopy.entity.file.ImageHashEntity;
import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.DuplicateImageException; import ru.soune.nocopy.exception.DuplicateImageException;
import ru.soune.nocopy.repository.*; import ru.soune.nocopy.repository.*;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import ru.soune.nocopy.util.FileUtil; import ru.soune.nocopy.util.FileUtil;
import java.io.File; import java.io.File;
@@ -41,6 +42,8 @@ public class FileSimilarityService {
private final FileEntityRepository fileEntityRepository; private final FileEntityRepository fileEntityRepository;
private final CloudStorageService cloudStorageService;
@Value("${server.baseurl}") @Value("${server.baseurl}")
private String baseUrl; private String baseUrl;
@@ -114,7 +117,7 @@ public class FileSimilarityService {
} }
public FileEntity findDuplicateByHash(String path, String mimeType, Long userId) throws Exception { public FileEntity findDuplicateByHash(String path, String mimeType, Long userId) throws Exception {
String hash = calculateFileHash(path); String hash = calculateFileHash(path, false);
User user = userRepository.findById(userId).orElseThrow(); User user = userRepository.findById(userId).orElseThrow();
Company company = user.getCompany(); Company company = user.getCompany();
@@ -129,15 +132,15 @@ public class FileSimilarityService {
List<FileEntity> fileEntityList = fileEntityRepository.findByMimeTypeAndUserIds(mimeType, userIds); List<FileEntity> fileEntityList = fileEntityRepository.findByMimeTypeAndUserIds(mimeType, userIds);
for (FileEntity file : fileEntityList) { for (FileEntity file : fileEntityList) {
if (file.getProtectedFilePath() == null) continue; // if (file.getProtectedFilePath() == null) continue;
File existingFile = new File(file.getProtectedFilePath()); // File existingFile = cloudStorageService.readFileFromStorageByPath(file.getFilePath());
File newFile = new File(path); // File newFile = new File(path);
if (existingFile.length() == newFile.length()) { // if (existingFile.length() == newFile.length()) {
if (calculateFileHash(file.getProtectedFilePath()).equals(hash)) { if (calculateFileHash(file.getFilePath(), true).equals(hash)) {
return file; return file;
} }
} // }
} }
return null; return null;
@@ -196,14 +199,19 @@ public class FileSimilarityService {
} }
private String calculateFileHash(String path) throws Exception { private String calculateFileHash(String path, boolean cloud) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256"); MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (InputStream is = new FileInputStream(path)) { if (cloud) {
byte[] buffer = new byte[8192]; byte[] data = cloudStorageService.readFileFromStorageBytes(path);
int read; digest.update(data);
while ((read = is.read(buffer)) > 0) { } else {
digest.update(buffer, 0, read); try (InputStream is = new FileInputStream(path)) {
byte[] buffer = new byte[8192];
int read;
while ((read = is.read(buffer)) > 0) {
digest.update(buffer, 0, read);
}
} }
} }
@@ -8,9 +8,11 @@ import org.springframework.stereotype.Service;
import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.ImageHashEntity; import ru.soune.nocopy.entity.file.ImageHashEntity;
import ru.soune.nocopy.repository.ImageHashRepository; import ru.soune.nocopy.repository.ImageHashRepository;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Map; import java.util.Map;
@@ -22,18 +24,50 @@ public class ImageHashService {
private final ImageHashRepository repository; private final ImageHashRepository repository;
public Map<String, Long> calculateHash(Path imagePath) throws IOException { private final CloudStorageService cloudStorageService;
File file = imagePath.toFile();
PHash pHash = PerceptualHashHelper.INSTANCE.generateDCTPerceptualHash(file); // public Map<String, Long> calculateHash(Path imagePath) throws IOException {
//// File file = cloudStorageService.readFileFromStorageByPath(imagePath.toString());
// File file = imagePath.toFile();
// if (file == null) {
// file = cloudStorageService.readFileFromStorageByPath(imagePath.toString());
// }
//
// PHash pHash = PerceptualHashHelper.INSTANCE.generateDCTPerceptualHash(file);
//
// Long firstPart = pHash.getFirstPart();
// Long secondPart = pHash.getSecondPart();
// Map<String, Long> hash = Map.of(
// "hi", firstPart,
// "low", secondPart);
//
// return hash;
// }
public Map<String, Long> calculateHash(Path imagePath) throws IOException {
PHash pHash;
if (imagePath.toFile().exists()) {
pHash = PerceptualHashHelper.INSTANCE.generateDCTPerceptualHash(imagePath.toFile());
} else {
byte[] data = cloudStorageService.readFileFromStorageBytes(imagePath.toString());
Path tempFile = Files.createTempFile("perceptual_hash_", ".tmp");
try {
Files.write(tempFile, data);
pHash = PerceptualHashHelper.INSTANCE.generateDCTPerceptualHash(tempFile.toFile());
} finally {
Files.deleteIfExists(tempFile);
}
}
Long firstPart = pHash.getFirstPart(); Long firstPart = pHash.getFirstPart();
Long secondPart = pHash.getSecondPart(); Long secondPart = pHash.getSecondPart();
Map<String, Long> hash = Map.of(
"hi", firstPart,
"low", secondPart);
return hash; return Map.of(
"hi", firstPart,
"low", secondPart
);
} }
public void create(FileEntity file, Map<String, Long> stringIntegerMap) { public void create(FileEntity file, Map<String, Long> stringIntegerMap) {
@@ -9,6 +9,7 @@ import org.springframework.stereotype.Service;
import ru.soune.nocopy.entity.complaint.LawCase; import ru.soune.nocopy.entity.complaint.LawCase;
import ru.soune.nocopy.entity.complaint.LawCasePriority; import ru.soune.nocopy.entity.complaint.LawCasePriority;
import ru.soune.nocopy.entity.complaint.LawCaseType; import ru.soune.nocopy.entity.complaint.LawCaseType;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.repository.LawCaseRepository; import ru.soune.nocopy.repository.LawCaseRepository;
import java.math.BigDecimal; import java.math.BigDecimal;
@@ -21,7 +22,7 @@ public class LawCaseService {
private final LawCaseRepository lawCaseRepository; private final LawCaseRepository lawCaseRepository;
public LawCase addLawCase(BigDecimal amount, String description, String name, public LawCase addLawCase(BigDecimal amount, String description, String name,
LawCasePriority priority) { LawCasePriority priority, Long violationId, User user) {
LawCase lawCase = new LawCase(); LawCase lawCase = new LawCase();
BigDecimal damage = amount == null ? BigDecimal.ZERO : amount; BigDecimal damage = amount == null ? BigDecimal.ZERO : amount;
@@ -30,6 +31,8 @@ public class LawCaseService {
lawCase.setName(name); lawCase.setName(name);
lawCase.setDescription(description); lawCase.setDescription(description);
lawCase.setPriority(priority); lawCase.setPriority(priority);
lawCase.setViolationId(violationId);
lawCase.setUser(user);
return lawCaseRepository.save(lawCase); return lawCaseRepository.save(lawCase);
} }
@@ -67,25 +70,16 @@ public class LawCaseService {
return lawCaseRepository.save(lawCase); return lawCaseRepository.save(lawCase);
} }
public List<LawCase> getAllUserLawCases(Long userId, int pageSize, int pageNumber, String propertySort,
String ascDesc) {
Sort.Direction direction = ascDesc.equalsIgnoreCase("desc") ? Sort.Direction.DESC:
Sort.Direction.ASC;
Sort sort = Sort.by(direction, propertySort);
Pageable pageable = PageRequest.of(pageNumber, pageSize, sort);
return lawCaseRepository.getUserLawCases(userId, pageable);
}
public Page<LawCase> getAllUserLawCases(Long userId, int pageSize, int pageNumber, public Page<LawCase> getAllUserLawCases(Long userId, int pageSize, int pageNumber,
LawCaseType lawCaseType, String lawyer, LawCaseType lawCaseType, String lawyer,
LawCasePriority lawCasePriority, LawCasePriority lawCasePriority,
Long violationId,
String propertySort, String ascDesc) { String propertySort, String ascDesc) {
Sort.Direction direction = ascDesc.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC; Sort.Direction direction = ascDesc.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC;
Sort sort = Sort.by(direction, propertySort); Sort sort = Sort.by(direction, propertySort);
Pageable pageable = PageRequest.of(pageNumber, pageSize, sort); Pageable pageable = PageRequest.of(pageNumber, pageSize, sort);
return lawCaseRepository.getUserLawCases(userId, lawCaseType, lawyer, lawCasePriority, pageable); return lawCaseRepository.getUserLawCases(userId, lawCaseType, lawyer, lawCasePriority, violationId, pageable);
} }
public void deleteById(Long id) { public void deleteById(Long id) {
@@ -8,6 +8,7 @@ import lombok.extern.slf4j.Slf4j;
import ru.soune.nocopy.entity.user.ProtectedFileCheck; import ru.soune.nocopy.entity.user.ProtectedFileCheck;
import ru.soune.nocopy.repository.ProtectedFileCheckRepository; import ru.soune.nocopy.repository.ProtectedFileCheckRepository;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.*; import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
@@ -15,7 +16,10 @@ import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Slf4j @Slf4j
@Component @Component
@@ -34,6 +38,8 @@ public class FileCleanupService {
public void cleanupExpiredFiles() { public void cleanupExpiredFiles() {
log.info("Starting cleanup of expired temporary files"); log.info("Starting cleanup of expired temporary files");
cleanupTempFilesNightly();
Path tempDir = Paths.get(basePath, "temp"); Path tempDir = Paths.get(basePath, "temp");
if (!Files.exists(tempDir)) { if (!Files.exists(tempDir)) {
return; return;
@@ -83,4 +89,50 @@ public class FileCleanupService {
log.error("Error during cleanup", e); log.error("Error during cleanup", e);
} }
} }
private void cleanupTempFilesNightly() {
log.info("Starting nightly cleanup of temporary files");
String tempDir = System.getProperty("java.io.tmpdir");
Path tempPath = Paths.get(tempDir);
if (!Files.exists(tempPath)) {
return;
}
Instant cutoffTime = Instant.now().minus(24, ChronoUnit.HOURS); // 24 часа
int deletedCount = 0;
try (Stream<Path> files = Files.list(tempPath)) {
List<Path> tempFiles = files
.filter(path -> {
String filename = path.getFileName().toString();
return filename.contains("_") &&
!filename.endsWith(".tmp") &&
Files.isRegularFile(path);
})
.collect(Collectors.toList());
for (Path filePath : tempFiles) {
try {
File file = filePath.toFile();
Instant lastModified = Instant.ofEpochMilli(file.lastModified());
if (lastModified.isBefore(cutoffTime)) {
boolean deleted = Files.deleteIfExists(filePath);
if (deleted) {
deletedCount++;
}
}
} catch (IOException e) {
log.error("Error deleting file: {}", filePath, e);
}
}
log.info("Nightly cleanup deleted {} temporary files", deletedCount);
} catch (IOException e) {
log.error("Failed to list temp directory", e);
}
}
} }
@@ -3,6 +3,8 @@ package ru.soune.nocopy.service.file;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.file.FileEntityResponse; import ru.soune.nocopy.dto.file.FileEntityResponse;
@@ -17,12 +19,15 @@ import ru.soune.nocopy.exception.FileEntityNotFoundException;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.FileMonitoringRepository; import ru.soune.nocopy.repository.FileMonitoringRepository;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -40,6 +45,8 @@ public class FileEntityService {
private final ImageResizeService imageResizeService; private final ImageResizeService imageResizeService;
private final CloudStorageService cloudStorageService;
@Value("${server.baseurl}") @Value("${server.baseurl}")
private String baseUrl; private String baseUrl;
@@ -104,6 +111,34 @@ public class FileEntityService {
return allFiles; return allFiles;
} }
@Transactional(readOnly = true)
public FileResponse getUserFiles(Long userId, Pageable pageable, int version) {
User user = userRepository.findById(userId).orElseThrow();
List<User> users = user.getCompany() != null ?
userRepository.findByCompanyId(user.getCompany().getId()) : List.of(user);
List<FileStatus> statusesForSearch = List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION);
Page<FileEntity> files = fileEntityRepository.findFiles(users.stream().map(User::getId).toList(),
statusesForSearch, pageable);
Long totalSize = fileEntityRepository.sumFileSize(
users.stream().map(User::getId).toList(),
statusesForSearch);
List<FileEntityResponse> filesToResponse = files.stream()
.map(file -> convertToResponse(file, version))
.toList();
return FileResponse.builder()
.files(filesToResponse)
.totalCount((int) files.getTotalElements())
.totalSize(totalSize != null ? totalSize : 0L)
.formattedTotalSize(formatFileSize(totalSize != null ? totalSize : 0L))
.page(pageable.getPageNumber())
.pageSize(pageable.getPageSize())
.build();
}
@Transactional(readOnly = true) @Transactional(readOnly = true)
public FileResponse getUserFiles(Long userId, int page, int pageSize, int version) { public FileResponse getUserFiles(Long userId, int page, int pageSize, int version) {
User user = userRepository.findById(userId).orElseThrow(); User user = userRepository.findById(userId).orElseThrow();
@@ -172,16 +207,8 @@ public class FileEntityService {
fileEntityRepository.save(fileEntity); fileEntityRepository.save(fileEntity);
} }
public void deleteFromDisk(FileEntity fileEntity) throws IOException { public void deleteFromDisk(String deleteFilePath) throws IOException {
Path path = Paths.get(fileEntity.getFilePath()); Files.deleteIfExists(Paths.get(deleteFilePath));
if (!Files.exists(path)) {
return;
}
Files.delete(path);
fileEntityRepository.delete(fileEntity);
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
@@ -207,18 +234,27 @@ public class FileEntityService {
} }
@Transactional @Transactional
public void writeProtectedFile(String id, byte[] data, String fileExt) throws IOException { public FileEntity writeProtectedFile(String id, byte[] data, String fileExt) throws IOException {
FileEntity fileEntity = fileEntityRepository.findById(id) FileEntity fileEntity = fileEntityRepository.findById(id)
.orElseThrow(() -> new RuntimeException("File not found: " + id)); .orElseThrow(() -> new RuntimeException("File not found: " + id));
String extension = determineFileExtension(fileExt, fileEntity); String extension = determineFileExtension(fileExt, fileEntity);
Path protectedFilePath = prepareProtectedPath(fileEntity, extension); Path protectedFilePath = prepareProtectedPath(fileEntity, extension);
if (Files.exists(protectedFilePath)) { Files.createDirectories(protectedFilePath.getParent());
Files.delete(protectedFilePath);
}
Files.write(protectedFilePath, data); Files.deleteIfExists(protectedFilePath);
try (FileChannel channel = FileChannel.open(protectedFilePath,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE)) {
ByteBuffer buffer = ByteBuffer.wrap(data);
while (buffer.hasRemaining()) {
channel.write(buffer);
}
channel.force(true);
}
if (imageResizeService.isImage(extension)) { if (imageResizeService.isImage(extension)) {
imageResizeService.generateSizes(fileEntity, data); imageResizeService.generateSizes(fileEntity, data);
@@ -230,14 +266,14 @@ public class FileEntityService {
fileEntity.setFileExtension(extension); fileEntity.setFileExtension(extension);
fileEntity.setProtectionStatus(ProtectionStatus.PROTECTED); fileEntity.setProtectionStatus(ProtectionStatus.PROTECTED);
fileEntityRepository.save(fileEntity); return fileEntityRepository.save(fileEntity);
} }
public void clearTempFiles(long userId) throws IOException { public void clearTempFiles(long userId) {
List<FileEntity> fileByUserIdAndStatus = fileEntityRepository.findFileByUserIdAndStatus(userId, FileStatus.TEMP); List<FileEntity> fileByUserIdAndStatus = fileEntityRepository.findFileByUserIdAndStatus(userId, FileStatus.TEMP);
for (FileEntity fileEntity : fileByUserIdAndStatus) { for (FileEntity fileEntity : fileByUserIdAndStatus) {
deleteFromDisk(fileEntity); cloudStorageService.deleteFromStorageDisk(fileEntity);
} }
} }
@@ -249,24 +285,6 @@ public class FileEntityService {
return fileEntityRepository.findFileIdByFilePath(filePath); return fileEntityRepository.findFileIdByFilePath(filePath);
} }
public File getFileById(String id) {
try {
FileEntity fileEntity = fileEntityRepository.findById(id).orElseThrow(() ->
new RuntimeException("File not found: " + id));
File file = new File(fileEntity.getFilePath());
if (!file.exists()) {
throw new RuntimeException("File not found on disk: " + fileEntity.getFilePath());
}
return file;
} catch (Exception e) {
log.error("Error getting file: {}", id, e);
return null;
}
}
public Path prepareProtectedPath(FileEntity fileEntity, String extension) throws IOException { public Path prepareProtectedPath(FileEntity fileEntity, String extension) throws IOException {
Path originalPath = Paths.get(fileEntity.getFilePath()); Path originalPath = Paths.get(fileEntity.getFilePath());
@@ -292,7 +310,7 @@ public class FileEntityService {
return protectedPath; return protectedPath;
} }
public void updateSignature(String signature, String fileId) throws IOException { public FileEntity updateSignature(String signature, String fileId) throws IOException {
FileEntity fileEntity = fileEntityRepository.findByFileId(fileId); FileEntity fileEntity = fileEntityRepository.findByFileId(fileId);
fileEntity.setSignature(signature); fileEntity.setSignature(signature);
fileEntity.setProtectionStatus(ProtectionStatus.PROTECTED); fileEntity.setProtectionStatus(ProtectionStatus.PROTECTED);
@@ -300,18 +318,30 @@ public class FileEntityService {
fileEntity.setProtectedAt(LocalDateTime.now()); fileEntity.setProtectedAt(LocalDateTime.now());
fileEntity.setProtectedFilePath(prepareProtectedPath(fileEntity, fileEntity.getFileExtension()).toString()); fileEntity.setProtectedFilePath(prepareProtectedPath(fileEntity, fileEntity.getFileExtension()).toString());
fileEntityRepository.save(fileEntity); return fileEntityRepository.save(fileEntity);
} }
private boolean checkFileExistsOnDisk(String filePath) { public String formatFileSize(long size) {
try { if (size < 1024) {
return Files.exists(Paths.get(filePath)); return size + " B";
} catch (Exception e) { } else if (size < 1024 * 1024) {
log.warn("Error checking file existence: {}", filePath, e); return String.format("%.1f KB", size / 1024.0);
return false; } else if (size < 1024 * 1024 * 1024) {
return String.format("%.1f MB", size / (1024.0 * 1024.0));
} else {
return String.format("%.1f GB", size / (1024.0 * 1024.0 * 1024.0));
} }
} }
// private boolean checkFileExistsOnDisk(String filePath) {
// try {
// return Files.exists(Paths.get(filePath));
// } catch (Exception e) {
// log.warn("Error checking file existence: {}", filePath, e);
// return false;
// }
// }
private String determineFileExtension(String fileExt, FileEntity fileEntity) { private String determineFileExtension(String fileExt, FileEntity fileEntity) {
if (fileExt != null) { if (fileExt != null) {
return fileExt; return fileExt;
@@ -323,7 +353,7 @@ public class FileEntityService {
private final FileMonitoringRepository fileMonitoringRepository; private final FileMonitoringRepository fileMonitoringRepository;
private FileEntityResponse convertToResponse(FileEntity fileEntity, int version) { private FileEntityResponse convertToResponse(FileEntity fileEntity, int version) {
boolean existsOnDisk = checkFileExistsOnDisk(fileEntity.getFilePath()); // boolean existsOnDisk = checkFileExistsOnDisk(fileEntity.getFilePath());
User user = userRepository.findById(fileEntity.getUserId()).get(); User user = userRepository.findById(fileEntity.getUserId()).get();
String fileName = fileEntity.getOriginalFileName(); String fileName = fileEntity.getOriginalFileName();
String name = fileName; String name = fileName;
@@ -355,7 +385,7 @@ public class FileEntityService {
.updatedAt(fileEntity.getUpdatedAt()) .updatedAt(fileEntity.getUpdatedAt())
.formattedSize(formatFileSize(fileEntity.getFileSize())) .formattedSize(formatFileSize(fileEntity.getFileSize()))
.downloadUrl("/api/v" + version + "/files/download/" + fileEntity.getId()) .downloadUrl("/api/v" + version + "/files/download/" + fileEntity.getId())
.existsOnDisk(existsOnDisk) // .existsOnDisk(existsOnDisk)
.supportId(fileEntity.getSupportId()) .supportId(fileEntity.getSupportId())
.protectStatus(fileEntity.getProtectionStatus().toString()) .protectStatus(fileEntity.getProtectionStatus().toString())
.fileName(name + "_nocopy_protected" + "." + fileEntity.getFileExtension()) .fileName(name + "_nocopy_protected" + "." + fileEntity.getFileExtension())
@@ -370,16 +400,4 @@ public class FileEntityService {
.thumbnailFileUrl(baseUrl + "/api/files/protected/" + fileEntity.getId() + "/thumbnail") .thumbnailFileUrl(baseUrl + "/api/files/protected/" + fileEntity.getId() + "/thumbnail")
.build(); .build();
} }
public String formatFileSize(long size) {
if (size < 1024) {
return size + " B";
} else if (size < 1024 * 1024) {
return String.format("%.1f KB", size / 1024.0);
} else if (size < 1024 * 1024 * 1024) {
return String.format("%.1f MB", size / (1024.0 * 1024.0));
} else {
return String.format("%.1f GB", size / (1024.0 * 1024.0 * 1024.0));
}
}
} }
@@ -25,8 +25,12 @@ public class FileStatsService {
private final UserRepository userRepository; private final UserRepository userRepository;
private final FileEntityService fileEntityService;
public FileInfoUserResponse getUserFileStats(Long userId) { public FileInfoUserResponse getUserFileStats(Long userId) {
List<FileEntity> userFiles = fileEntityRepository.findByUserId(userId); List<FileEntity> userFiles = fileEntityService.getAllUserFiles(userId, List.of(FileStatus.ACTIVE,
FileStatus.BLOCKED, FileStatus.MODERATION));
return calculateStats(userFiles); return calculateStats(userFiles);
} }
@@ -34,32 +38,24 @@ public class FileStatsService {
return FileInfoUserResponse.builder() return FileInfoUserResponse.builder()
.allFileSize(calculateTotalSize(files)) .allFileSize(calculateTotalSize(files))
.fileCount(calculateTotalCount(files)) .fileCount(calculateTotalCount(files))
.filesCheck(calculateByStatus(files, FileStatus.CHECKED)) // .filesCheck(calculateByStatus(files, FileStatus.CHECKED))
.filesViolation(calculateByStatus(files, FileStatus.VIOLATION)) // .filesViolation(calculateByStatus(files, FileStatus.VIOLATION))
.protectedFilesCount(protectedUserFiles(files)) .protectedFilesCount(protectedUserFiles(files))
.imagesSize(calculateMediaSize(files, FileType.IMAGE)) .imagesSize(calculateMediaSize(files, FileType.IMAGE))
.imagesCount(calculateMediaCount(files, FileType.IMAGE)) .imagesCount(calculateMediaCount(files, FileType.IMAGE))
.imagesCheck(calculateMediaByStatus(files, FileType.IMAGE, FileStatus.CHECKED))
.imagesViolations(calculateMediaByStatus(files, FileType.IMAGE, FileStatus.VIOLATION))
.protectedImageFilesCount(protectedUserFiles(files, FileType.IMAGE)) .protectedImageFilesCount(protectedUserFiles(files, FileType.IMAGE))
.videosSize(calculateMediaSize(files, FileType.VIDEO)) .videosSize(calculateMediaSize(files, FileType.VIDEO))
.videosCount(calculateMediaCount(files, FileType.VIDEO)) .videosCount(calculateMediaCount(files, FileType.VIDEO))
.videosCheck(calculateMediaByStatus(files, FileType.VIDEO, FileStatus.CHECKED))
.videosViolations(calculateMediaByStatus(files, FileType.VIDEO, FileStatus.VIOLATION))
.protectedVideoFilesCount(protectedUserFiles(files, FileType.VIDEO)) .protectedVideoFilesCount(protectedUserFiles(files, FileType.VIDEO))
.audiosSize(calculateMediaSize(files, FileType.AUDIO)) .audiosSize(calculateMediaSize(files, FileType.AUDIO))
.audiosCount(calculateMediaCount(files, FileType.AUDIO)) .audiosCount(calculateMediaCount(files, FileType.AUDIO))
.audiosCheck(calculateMediaByStatus(files, FileType.AUDIO, FileStatus.CHECKED))
.audiosViolations(calculateMediaByStatus(files, FileType.AUDIO, FileStatus.VIOLATION))
.protectedAudioFilesCount(protectedUserFiles(files, FileType.AUDIO)) .protectedAudioFilesCount(protectedUserFiles(files, FileType.AUDIO))
.documentSize(calculateMediaSize(files, FileType.DOCUMENT)) .documentSize(calculateMediaSize(files, FileType.DOCUMENT))
.documentCount(calculateMediaCount(files, FileType.DOCUMENT)) .documentCount(calculateMediaCount(files, FileType.DOCUMENT))
.documentCheck(calculateMediaByStatus(files, FileType.DOCUMENT, FileStatus.CHECKED))
.documentViolations(calculateMediaByStatus(files, FileType.DOCUMENT, FileStatus.VIOLATION))
.protectedDocumentFilesCount(protectedUserFiles(files, FileType.DOCUMENT)) .protectedDocumentFilesCount(protectedUserFiles(files, FileType.DOCUMENT))
.build(); .build();
} }
@@ -2,11 +2,10 @@ package ru.soune.nocopy.service.file;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import ru.soune.nocopy.dto.file.UploadProgressResponse; import ru.soune.nocopy.dto.file.UploadProgressResponse;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.FileUploadSession; import ru.soune.nocopy.entity.file.FileUploadSession;
import java.io.IOException;
public interface FileUploadService { public interface FileUploadService {
FileUploadSession initUpload(Long userId, String fileName, FileUploadSession initUpload(Long userId, String fileName,
String fileType, String extension, String convertTo, long fileSize); String fileType, String extension, String convertTo, long fileSize);
@@ -22,5 +21,5 @@ public interface FileUploadService {
UploadProgressResponse getUploadProgress(String uploadId); UploadProgressResponse getUploadProgress(String uploadId);
void completeFileProcessingAsync(FileUploadSession session, FileStatus status); FileEntity completeFileProcessing(FileUploadSession session, FileStatus status);
} }
@@ -1,10 +1,12 @@
package ru.soune.nocopy.service.file; package ru.soune.nocopy.service.file;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import java.awt.*; import java.awt.*;
@@ -18,9 +20,13 @@ import java.util.List;
@Service @Service
@Slf4j @Slf4j
@RequiredArgsConstructor
public class ImageResizeService { public class ImageResizeService {
private final CloudStorageService cloudStorageService;
private static final int THUMBNAIL_SIZE = 150; private static final int THUMBNAIL_SIZE = 150;
private static final int MEDIUM_SIZE = 600; private static final int MEDIUM_SIZE = 600;
@Value("${file.storage.base-path}") @Value("${file.storage.base-path}")
@@ -50,11 +56,15 @@ public class ImageResizeService {
if (original.getWidth() > THUMBNAIL_SIZE) { if (original.getWidth() > THUMBNAIL_SIZE) {
String thumbPath = resizeAndSave(original, baseName + "_thumb", THUMBNAIL_SIZE); String thumbPath = resizeAndSave(original, baseName + "_thumb", THUMBNAIL_SIZE);
file.setThumbnailPath(thumbPath); file.setThumbnailPath(thumbPath);
cloudStorageService.saveFilesToStorage(file.getMimeType(), file.getFileExtension(),
thumbPath);
} }
if (original.getWidth() > MEDIUM_SIZE) { if (original.getWidth() > MEDIUM_SIZE) {
String mediumPath = resizeAndSave(original, baseName + "_medium", MEDIUM_SIZE); String mediumPath = resizeAndSave(original, baseName + "_medium", MEDIUM_SIZE);
file.setMediumPath(mediumPath); file.setMediumPath(mediumPath);
cloudStorageService.saveFilesToStorage(file.getMimeType(), file.getFileExtension(),
mediumPath);
} }
} }
} }
@@ -0,0 +1,194 @@
package ru.soune.nocopy.service.file.cloud;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.repository.FileEntityRepository;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.ResponseBytes;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.core.sync.ResponseTransformer;
import software.amazon.awssdk.http.apache.ApacheHttpClient;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.UUID;
@Service
@Slf4j
@RequiredArgsConstructor
public class CloudStorageService {
private final FileEntityRepository fileEntityRepository;
@Value("${yandex.cloud.key}")
private String key;
@Value("${yandex.cloud.secret-key}")
private String secretKey;
@Value("${yandex.cloud.region}")
private String region;
@Value("${yandex.cloud.s3-endpoint}")
private String s3endpoint;
@Value("${yandex.cloud.bucket}")
private String bucket;
public void saveFilesToStorage(String mimeType, String extension, String sendToCloudFilePath) {
AwsCredentials credentials = AwsBasicCredentials.create(key, secretKey);
S3Client s3Client = S3Client.builder()
.httpClient(ApacheHttpClient.create())
.region(Region.of(region))
.endpointOverride(URI.create(s3endpoint))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.build();
String mimeTypeFromFile = mimeType.equals("document") ? "application" : mimeType;
String contentType = mimeTypeFromFile + "/" + extension;
String filePath = "files" + sendToCloudFilePath;
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucket)
.key(filePath)
.contentType(contentType)
.build();
try {
byte[] bytes = Files.readAllBytes(Paths.get(sendToCloudFilePath));
s3Client.putObject(putObjectRequest,
RequestBody.fromBytes(bytes));
Files.deleteIfExists(Paths.get(sendToCloudFilePath));
} catch (IOException e) {
log.error("Failed to read file: {}", filePath, e);
}
}
public File readFileFromStorageByPath(String filePath) throws IOException {
GetObjectRequest objectRequest = GetObjectRequest.builder()
.bucket(bucket)
.key("files" + filePath)
.build();
AwsCredentials credentials = AwsBasicCredentials.create(key, secretKey);
S3Client s3Client = S3Client.builder()
.httpClient(ApacheHttpClient.create())
.region(Region.of(region))
.endpointOverride(URI.create(s3endpoint))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.build();
String tempDir = System.getProperty("java.io.tmpdir");
String fileName = System.currentTimeMillis() + "_" + UUID.randomUUID() + "_" +
filePath.replace("/", "_");
File tempFile = new File(tempDir, fileName);
s3Client.getObject(objectRequest, tempFile.toPath());
tempFile.deleteOnExit();
return tempFile;
}
public byte[] readFileFromStorageBytes(String filePath) {
AwsCredentials credentials = AwsBasicCredentials.create(key, secretKey);
S3Client s3Client = S3Client.builder()
.httpClient(ApacheHttpClient.create())
.region(Region.of(region))
.endpointOverride(URI.create(s3endpoint))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.build();
GetObjectRequest objectRequest = GetObjectRequest.builder()
.bucket(bucket)
.key("files" + filePath)
.build();
try {
ResponseBytes<GetObjectResponse> responseBytes = s3Client.getObjectAsBytes(objectRequest);
return responseBytes.asByteArray();
} catch (Exception e) {
log.error("Failed to read file from S3: {}", filePath, e);
throw new RuntimeException("Failed to read file from S3", e);
}
}
public InputStream readFileFromStorage(String filePath) {
GetObjectRequest objectRequest = GetObjectRequest.builder()
.bucket(bucket)
.key("files" + filePath)
.build();
AwsCredentials credentials = AwsBasicCredentials.create(key, secretKey);
S3Client s3Client = S3Client.builder()
.httpClient(ApacheHttpClient.create())
.region(Region.of(region))
.endpointOverride(URI.create(s3endpoint))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.build();
return s3Client.getObject(objectRequest, ResponseTransformer.toInputStream());
}
public void deleteFileFromStorage(String filePath) {
if (filePath == null || filePath.isEmpty()) {
log.warn("Attempted to delete file with empty path");
return;
}
AwsCredentials credentials = AwsBasicCredentials.create(key, secretKey);
S3Client s3Client = S3Client.builder()
.httpClient(ApacheHttpClient.create())
.region(Region.of(region))
.endpointOverride(URI.create(s3endpoint))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.build();
String cloudKey = "files" + filePath;
try {
try {
s3Client.headObject(builder -> builder.bucket(bucket).key(cloudKey).build());
} catch (Exception e) {
log.warn("File does not exist in cloud storage: {}", cloudKey);
return;
}
s3Client.deleteObject(builder -> builder.bucket(bucket).key(cloudKey).build());
log.info("File successfully deleted from cloud storage: {}", cloudKey);
} catch (Exception e) {
log.error("Failed to delete file from cloud storage: {}", cloudKey, e);
throw new RuntimeException("Failed to delete file from cloud storage: " + cloudKey, e);
}
}
public void deleteFromStorageDisk(FileEntity fileEntity) {
deleteFileFromStorage(fileEntity.getFilePath());
if (fileEntity.getProtectedFilePath() != null) {
deleteFileFromStorage(fileEntity.getProtectedFilePath());
}
fileEntityRepository.delete(fileEntity);
}
}
@@ -9,6 +9,7 @@ import org.jetbrains.annotations.Nullable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import ru.soune.nocopy.util.FileUtil; import ru.soune.nocopy.util.FileUtil;
import java.io.IOException; import java.io.IOException;
@@ -22,6 +23,8 @@ public class AudioLocalSearchImpl implements AudioLocalSearch {
private final FileUtil fileUtil; private final FileUtil fileUtil;
private final CloudStorageService cloudStorageService;
@Override @Override
public @Nullable FileProtector.FileInfo findBySignature(@NotNull String signature) { public @Nullable FileProtector.FileInfo findBySignature(@NotNull String signature) {
FileEntity fileEntity = fileEntityService.findBySignature(signature); FileEntity fileEntity = fileEntityService.findBySignature(signature);
@@ -34,7 +37,10 @@ public class AudioLocalSearchImpl implements AudioLocalSearch {
@Override @Override
public void updateSignature(@NotNull String signature, @NotNull FileProtector.FileInfo fileInfo) { public void updateSignature(@NotNull String signature, @NotNull FileProtector.FileInfo fileInfo) {
try { try {
fileEntityService.updateSignature(signature, fileInfo.getId()); FileEntity fileEntity = fileEntityService.updateSignature(signature, fileInfo.getId());
cloudStorageService.saveFilesToStorage(fileEntity.getMimeType(), fileEntity.getFileExtension(),
fileEntity.getProtectedFilePath());
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@@ -25,6 +25,23 @@ public class DocumentLocalSearchImpl implements DocumentLocalSearch {
FileEntity fileEntity = fileEntityRepository.findByIdAndUserId(fileId, Long.valueOf(ownerId)); FileEntity fileEntity = fileEntityRepository.findByIdAndUserId(fileId, Long.valueOf(ownerId));
FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity, null); FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity, null);
log.info("fileInfo: {}", fileInfo); log.info("fileInfo: {}", fileInfo);
log.info("fileInfo: {}", fileInfo);
log.info("fileInfo: {}", fileInfo);
log.info("fileInfo: {}", fileInfo);
log.info("fileInfo: {}", fileInfo);
log.info("fileInfo: {}", fileInfo);
log.info("fileInfo: {}", fileInfo);
log.info("fileInfo: {}", fileInfo);
log.info("fileInfo: {}", fileInfo);
log.info("fileInfo: {}", fileInfo);
log.info("fileInfo: {}", fileInfo);
log.info("fileInfo: {}", fileInfo);
log.info("fileInfo: {}", fileInfo);
log.info("fileInfo: {}", fileInfo);
log.info("fileInfo: {}", fileInfo);
log.info("fileInfo: {}", fileInfo);
log.info("fileInfo: {}", fileInfo);
log.info("fileInfo: {}", fileInfo);
return fileInfo; return fileInfo;
} }
@@ -5,8 +5,8 @@ import com.vrt.fileprotection.FileProtector;
import com.vrt.fileprotection.NoCopyCheckResult; import com.vrt.fileprotection.NoCopyCheckResult;
import com.vrt.fileprotection.audio.AudioCheckResult; import com.vrt.fileprotection.audio.AudioCheckResult;
import com.vrt.fileprotection.documents.DocumentCheckResult; import com.vrt.fileprotection.documents.DocumentCheckResult;
import com.vrt.fileprotection.image.ImageCheckResult;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -19,6 +19,7 @@ import org.springframework.web.multipart.MultipartFile;
import ru.soune.nocopy.dto.file.UploadProgressResponse; import ru.soune.nocopy.dto.file.UploadProgressResponse;
import ru.soune.nocopy.entity.file.*; import ru.soune.nocopy.entity.file.*;
import ru.soune.nocopy.entity.notification.NotificationType; import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.entity.tokenoperation.OperationType;
import ru.soune.nocopy.exception.*; import ru.soune.nocopy.exception.*;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.FileUploadSessionRepository; import ru.soune.nocopy.repository.FileUploadSessionRepository;
@@ -28,6 +29,8 @@ import ru.soune.nocopy.service.ImageHashService;
import ru.soune.nocopy.service.cost.CostService; import ru.soune.nocopy.service.cost.CostService;
import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.file.FileUploadService; import ru.soune.nocopy.service.file.FileUploadService;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import ru.soune.nocopy.service.tariff.TariffConstants;
import ru.soune.nocopy.service.file.moderation.ModerationFileService; import ru.soune.nocopy.service.file.moderation.ModerationFileService;
import ru.soune.nocopy.service.notification.NotificationService; import ru.soune.nocopy.service.notification.NotificationService;
import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.register.AuthService;
@@ -95,6 +98,8 @@ public class FileUploadServiceImpl implements FileUploadService {
private final ModerationFileService moderationFileService; private final ModerationFileService moderationFileService;
private final CloudStorageService cloudStorageService;
@PostConstruct @PostConstruct
public void init() { public void init() {
try { try {
@@ -190,9 +195,8 @@ public class FileUploadServiceImpl implements FileUploadService {
} }
@Override @Override
@Transactional
public UploadProgressResponse uploadChunk(String uploadId, Integer chunkNumber, public UploadProgressResponse uploadChunk(String uploadId, Integer chunkNumber,
MultipartFile chunkFile, Integer findSimilar) { MultipartFile chunkFile, Integer findSimilar) {
FileUploadSession session = sessionRepository.findById(uploadId) FileUploadSession session = sessionRepository.findById(uploadId)
.orElseThrow(() -> new UploadSessionNotFoundException(uploadId)); .orElseThrow(() -> new UploadSessionNotFoundException(uploadId));
@@ -222,10 +226,9 @@ public class FileUploadServiceImpl implements FileUploadService {
return UploadProgressResponse.fromSession(session); return UploadProgressResponse.fromSession(session);
} }
@Async("fileUploadTaskExecutor")
@Transactional @Transactional
@Override @Override
public void completeFileProcessingAsync(FileUploadSession session, FileStatus status) { public FileEntity completeFileProcessing(FileUploadSession session, FileStatus status) {
try { try {
Path filePath = Paths.get(session.getFilePath()); Path filePath = Paths.get(session.getFilePath());
String checksum = calculateChecksum(filePath); String checksum = calculateChecksum(filePath);
@@ -240,30 +243,28 @@ public class FileUploadServiceImpl implements FileUploadService {
.fileExtension(session.getExtension()) .fileExtension(session.getExtension())
.checksum(checksum) .checksum(checksum)
.uploadSessionId(session.getUploadId()) .uploadSessionId(session.getUploadId())
.protectionStatus(ProtectionStatus.PROCESSING)
.status(status) .status(status)
.build(); .build();
if (status != FileStatus.TEMP) {
fileEntity.setProtectionStatus(ProtectionStatus.PROCESSING);
}
FileEntity saved = fileEntityRepository.save(fileEntity); FileEntity saved = fileEntityRepository.save(fileEntity);
if (session.getFileType().equals("image") && status != FileStatus.PRIVATE) { if (session.getFileType().equals("image") && status != FileStatus.PRIVATE) {
Map<String, Long> hash = imageHashService.calculateHash(filePath); Map<String, Long> hash = imageHashService.calculateHash(filePath);
imageHashService.create(saved, hash); imageHashService.create(saved, hash);
} }
cleanupSessionFiles(session); cleanupSessionFiles(session);
if (status != FileStatus.TEMP && status != FileStatus.PRIVATE) {
FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity, session.getConvertTo());
noCopyFileService.addFile(fileInfo);
}
log.info("File processing completed for session: {}", session.getUploadId()); log.info("File processing completed for session: {}", session.getUploadId());
return saved;
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to complete file processing for session {}: {}", log.error("Failed to complete file processing for session {}: {}",
session.getUploadId(), e.getMessage(), e); session.getUploadId(), e.getMessage(), e);
throw new FileUploadException("Failed to complete file processing", e);
} }
} }
@@ -336,7 +337,7 @@ public class FileUploadServiceImpl implements FileUploadService {
session.setStatus(UploadStatus.COMPLETED); session.setStatus(UploadStatus.COMPLETED);
session.setFilePath(finalFilePath); session.setFilePath(finalFilePath);
completeFileProcessingAsync(session, FileStatus.PRIVATE); completeFileProcessing(session, FileStatus.PRIVATE);
} else { } else {
sessionRepository.save(session); sessionRepository.save(session);
} }
@@ -378,18 +379,19 @@ public class FileUploadServiceImpl implements FileUploadService {
chunkNumber, session.getChunksUploaded(), session.getTotalChunks()); chunkNumber, session.getChunksUploaded(), session.getTotalChunks());
boolean isLastChunk = session.getChunksUploaded().equals(session.getTotalChunks()); boolean isLastChunk = session.getChunksUploaded().equals(session.getTotalChunks());
//TODO CHECK
if (isLastChunk) { if (isLastChunk) {
String finalFilePath = assembleFileSynchronously(session); String finalFilePath = assembleFileSynchronously(session);
if (findSimilar == 0) { if (findSimilar == 0) {
// if (session.getFileType().startsWith("image")) { if (session.getFileType().startsWith("image")) {
// checkForDuplicatesSynchronously(finalFilePath); checkForDuplicatesSynchronously(finalFilePath);
// } else if (session.getFileType().startsWith("audio") || session.getFileType().startsWith("document")) { } else if (session.getFileType().startsWith("audio") || session.getFileType().startsWith("document")) {
fileSimilarityService.hasDuplicatesByHash(finalFilePath ,session.getFileType(), fileSimilarityService.hasDuplicatesByHash(finalFilePath ,session.getFileType(),
session.getUserId()); session.getUserId());
File file = new File(finalFilePath); File file = new File(finalFilePath);
// File file = cloudStorageService.readFileFromStorageByPath(finalFilePath);
if (session.getFileType().startsWith("audio") && !isWavFile(file)) { if (session.getFileType().startsWith("audio") && !isWavFile(file)) {
session.setStatus(UploadStatus.FAILED); session.setStatus(UploadStatus.FAILED);
@@ -400,12 +402,9 @@ public class FileUploadServiceImpl implements FileUploadService {
throw new FileFormatException("Failed to upload chunk: .wav file not have RIFF header or " + throw new FileFormatException("Failed to upload chunk: .wav file not have RIFF header or " +
"another problems with .wav file"); "another problems with .wav file");
} }
FileProtector.Type type = FileProtector.Type.IMAGE;
if (session.getFileType().startsWith("audio")) type = FileProtector.Type.AUDIO; FileProtector.Type type = "document".equals(session.getFileType()) ?
if (session.getFileType().startsWith("document")) type = FileProtector.Type.DOC; FileProtector.Type.DOC : FileProtector.Type.valueOf(session.getFileType().toUpperCase());
// FileProtector.Type type = "document".equals(session.getFileType()) ?
// FileProtector.Type.DOC : FileProtector.Type.valueOf(session.getFileType().toUpperCase());
NoCopyCheckResult checkResult = noCopyFileService.checkFile(file, type); NoCopyCheckResult checkResult = noCopyFileService.checkFile(file, type);
@@ -415,45 +414,46 @@ public class FileUploadServiceImpl implements FileUploadService {
throw new DuplicateImageException("Duplicate", info.getId(), throw new DuplicateImageException("Duplicate", info.getId(),
Long.valueOf(info.getOwnerId())); Long.valueOf(info.getOwnerId()));
} }
case AudioCheckResult.Success success -> { case AudioCheckResult.Success success -> {
FileProtector.FileInfo info = success.getInfo(); FileProtector.FileInfo info = success.getInfo();
throw new DuplicateImageException("Duplicate", info.getId(), throw new DuplicateImageException("Duplicate", info.getId(),
Long.valueOf(info.getOwnerId())); Long.valueOf(info.getOwnerId()));
} }
case ImageCheckResult.Success success -> {
FileProtector.FileInfo info = success.getInfo();
throw new DuplicateImageException("Duplicate", info.getId(),
Long.valueOf(info.getOwnerId()));
}
case DocumentCheckResult.Failed failed -> case DocumentCheckResult.Failed failed ->
log.warn("Document check failed: {}", failed.getMessage()); log.warn("Document check failed: {}", failed.getMessage());
case AudioCheckResult.Failed failed -> case AudioCheckResult.Failed failed ->
log.warn("Audio check failed: {}", failed.getMessage()); log.warn("Audio check failed: {}", failed.getMessage());
default -> log.warn("Unexpected result type: {}", checkResult.getClass().getSimpleName()); default -> log.warn("Unexpected result type: {}", checkResult.getClass().getSimpleName());
} }
// } }
} }
session.setStatus(UploadStatus.COMPLETED); session.setStatus(UploadStatus.COMPLETED);
session.setFilePath(finalFilePath); session.setFilePath(finalFilePath);
notificationService.addNotification(NotificationType.FILE_ADDED_TO_SYSTEM, session.getUserId());
FileStatus status = findSimilar == 0 ? FileStatus.MODERATION: FileStatus.TEMP; FileStatus status = findSimilar == 0 ? FileStatus.MODERATION: FileStatus.TEMP;
if (status == FileStatus.TEMP) { if (status == FileStatus.TEMP) {
fileEntityService.clearTempFiles(session.getUserId()); fileEntityService.clearTempFiles(session.getUserId());
} }
completeFileProcessingAsync(session, status); FileEntity fileEntity = completeFileProcessing(session, status);
cloudStorageService.saveFilesToStorage(fileEntity.getMimeType(), fileEntity.getFileExtension(),
fileEntity.getFilePath());
if (status != FileStatus.TEMP) { if (status != FileStatus.TEMP) {
String fileType = session.getFileType(); String fileType = session.getFileType();
tariffInfoService.writeOffTokens(session.getUserId(), costService.protectFileCost(fileType)); tariffInfoService.writeOffTokens(session.getUserId(), costService.protectFileCost(fileType),
OperationType.FILE_UPLOAD);
notificationService.addNotification(NotificationType.FILE_ADDED_TO_SYSTEM, session.getUserId());
}
if (status != FileStatus.TEMP) {
FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity, session.getConvertTo());
noCopyFileService.addFile(fileInfo);
} }
} else { } else {
sessionRepository.save(session); sessionRepository.save(session);
@@ -588,6 +588,7 @@ public class FileUploadServiceImpl implements FileUploadService {
private void checkForDuplicatesSynchronously(String filePath) private void checkForDuplicatesSynchronously(String filePath)
throws IOException , DuplicateImageException { throws IOException , DuplicateImageException {
Path path = Paths.get(filePath); Path path = Paths.get(filePath);
Map<String, Long> hash = imageHashService.calculateHash(path); Map<String, Long> hash = imageHashService.calculateHash(path);
List<SimilarImageProjection> duplicates = fileSimilarityService.findDuplicatedByHash( List<SimilarImageProjection> duplicates = fileSimilarityService.findDuplicatedByHash(
@@ -11,7 +11,7 @@ import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.FileUploadSessionRepository; import ru.soune.nocopy.repository.FileUploadSessionRepository;
import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.file.FileUploadService; import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -32,27 +32,53 @@ public class ProtectionFileProviderImpl implements FileProtector.FileProvider {
private final FileEntityRepository fileEntityRepository; private final FileEntityRepository fileEntityRepository;
private final CloudStorageService cloudStorageService;
@Nullable @Nullable
@Override @Override
public File getImageFile(@NotNull String id) { public File getImageFile(@NotNull String id) {
return fileEntityService.getFileById(id); FileEntity fileEntity = fileEntityRepository.findById(id).orElseThrow(() ->
new RuntimeException("File not found: " + id));
try {
return cloudStorageService.readFileFromStorageByPath(fileEntity.getFilePath());
} catch (IOException e) {
throw new RuntimeException(e);
}
} }
@Nullable @Nullable
@Override @Override
public File getVideoFile(@NotNull String id) { public File getVideoFile(@NotNull String id) {
return fileEntityService.getFileById(id); FileEntity fileEntity = fileEntityRepository.findById(id).orElseThrow(() ->
new RuntimeException("File not found: " + id));
try {
return cloudStorageService.readFileFromStorageByPath(fileEntity.getFilePath());
} catch (IOException e) {
throw new RuntimeException(e);
}
} }
@Nullable @Nullable
@Override @Override
public File getAudioFile(@NotNull String id) { public File getAudioFile(@NotNull String id) {
return fileEntityService.getFileById(id); FileEntity fileEntity = fileEntityRepository.findById(id).orElseThrow(() ->
new RuntimeException("File not found: " + id));
try {
return cloudStorageService.readFileFromStorageByPath(fileEntity.getFilePath());
} catch (IOException e) {
throw new RuntimeException(e);
}
} }
@Override @Override
public @Nullable File getDocument(@NotNull String id) { public @Nullable File getDocument(@NotNull String id) {
return fileEntityService.getFileById(id); FileEntity fileEntity = fileEntityRepository.findById(id).orElseThrow(() ->
new RuntimeException("File not found: " + id));
try {
return cloudStorageService.readFileFromStorageByPath(fileEntity.getFilePath());
} catch (IOException e) {
throw new RuntimeException(e);
}
} }
@Override @Override
@@ -87,7 +113,20 @@ public class ProtectionFileProviderImpl implements FileProtector.FileProvider {
@Override @Override
public com.vrt.fileprotection.OperationResult writeAudioFile(@NotNull String id, @NotNull byte[] data, @Nullable String fileExt) { public com.vrt.fileprotection.OperationResult writeAudioFile(@NotNull String id, @NotNull byte[] data, @Nullable String fileExt) {
try { try {
fileEntityService.writeProtectedFile(id, data, fileExt); FileEntity fileEntity = fileEntityService.writeProtectedFile(id, data, fileExt);
Path filePath = Paths.get(fileEntity.getProtectedFilePath());
if (!Files.exists(filePath)) {
log.error("File not found after write: {}", filePath);
return OperationResult.Companion.failure("File not found");
}
log.info("File ready for upload: {} (size: {} bytes)", filePath, Files.size(filePath));
cloudStorageService.saveFilesToStorage(fileEntity.getMimeType(), fileEntity.getFileExtension(),
fileEntity.getProtectedFilePath());
return OperationResult.Companion.success(); return OperationResult.Companion.success();
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to create protected file: {}", id, e); log.error("Failed to create protected file: {}", id, e);
@@ -99,7 +138,11 @@ public class ProtectionFileProviderImpl implements FileProtector.FileProvider {
@Override @Override
public com.vrt.fileprotection.OperationResult writeVideoFile(@NotNull String id, @NotNull byte[] data, @Nullable String fileExt) { public com.vrt.fileprotection.OperationResult writeVideoFile(@NotNull String id, @NotNull byte[] data, @Nullable String fileExt) {
try { try {
fileEntityService.writeProtectedFile(id, data, fileExt); FileEntity fileEntity = fileEntityService.writeProtectedFile(id, data, fileExt);
cloudStorageService.saveFilesToStorage(fileEntity.getMimeType(), fileEntity.getFileExtension(),
fileEntity.getProtectedFilePath());
return OperationResult.Companion.success(); return OperationResult.Companion.success();
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to create protected file: {}", id, e); log.error("Failed to create protected file: {}", id, e);
@@ -111,7 +154,11 @@ public class ProtectionFileProviderImpl implements FileProtector.FileProvider {
@Override @Override
public com.vrt.fileprotection.OperationResult writeImageFile(@NotNull String id, @NotNull byte[] data, @Nullable String fileExt) { public com.vrt.fileprotection.OperationResult writeImageFile(@NotNull String id, @NotNull byte[] data, @Nullable String fileExt) {
try { try {
fileEntityService.writeProtectedFile(id, data, fileExt); FileEntity fileEntity = fileEntityService.writeProtectedFile(id, data, fileExt);
cloudStorageService.saveFilesToStorage(fileEntity.getMimeType(), fileEntity.getFileExtension(),
fileEntity.getProtectedFilePath());
return OperationResult.Companion.success(); return OperationResult.Companion.success();
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to create protected file: {}", id, e); log.error("Failed to create protected file: {}", id, e);
@@ -122,7 +169,10 @@ public class ProtectionFileProviderImpl implements FileProtector.FileProvider {
@Override @Override
public @NotNull OperationResult writeDocumentFile(@NotNull String id, @NotNull byte[] data) { public @NotNull OperationResult writeDocumentFile(@NotNull String id, @NotNull byte[] data) {
try { try {
fileEntityService.writeProtectedFile(id, data, null); FileEntity fileEntity = fileEntityService.writeProtectedFile(id, data, null);
cloudStorageService.saveFilesToStorage(fileEntity.getMimeType(), fileEntity.getFileExtension(),
fileEntity.getProtectedFilePath());
return OperationResult.Companion.success(); return OperationResult.Companion.success();
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to create protected file: {}", id, e); log.error("Failed to create protected file: {}", id, e);
@@ -0,0 +1,66 @@
package ru.soune.nocopy.service.geo;
import com.maxmind.geoip2.DatabaseReader;
import com.maxmind.geoip2.exception.GeoIp2Exception;
import com.maxmind.geoip2.model.CountryResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.net.InetAddress;
@Service
@Slf4j
public class GeoCountryService {
private final DatabaseReader dbReader;
public GeoCountryService() throws IOException {
Resource resource = new ClassPathResource("geo/GeoLite2-Country.mmdb");
this.dbReader = new DatabaseReader.Builder(resource.getInputStream()).build();
}
public String getCountryName(String input) {
try {
CountryResponse response = extractCountry(input);
return response.getCountry().getName();
} catch (Exception e) {
log.error("Failed for input: {}", input, e);
return "UNKNOWN";
}
}
public String getCountryCode(String input) {
try {
CountryResponse response = extractCountry(input);
return response.getCountry().getIsoCode();
} catch (Exception e) {
log.error("Failed for input: {}", input, e);
return "UNKNOWN";
}
}
private CountryResponse extractCountry(String input) throws IOException, GeoIp2Exception {
String domain = extractDomain(input);
InetAddress ip = InetAddress.getByName(domain);
return dbReader.country(ip);
}
private String extractDomain(String input) {
if (input == null || input.isEmpty()) {
throw new IllegalArgumentException("Input is empty");
}
String cleaned = input.replaceAll("^https?://", "");
cleaned = cleaned.split("/")[0];
cleaned = cleaned.split(":")[0];
return cleaned;
}
}
@@ -15,6 +15,7 @@ import ru.soune.nocopy.entity.notification.NotificationMessage;
import ru.soune.nocopy.entity.notification.NotificationType; import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.entity.tarif.TariffInfo; import ru.soune.nocopy.entity.tarif.TariffInfo;
import ru.soune.nocopy.entity.tarif.TariffType; import ru.soune.nocopy.entity.tarif.TariffType;
import ru.soune.nocopy.entity.tokenoperation.OperationType;
import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.TariffNotFoundException; import ru.soune.nocopy.exception.TariffNotFoundException;
import ru.soune.nocopy.repository.FileMonitoringRepository; import ru.soune.nocopy.repository.FileMonitoringRepository;
@@ -64,9 +65,10 @@ public class MonitoringSearchService {
MonitoringType monitoringType = monitoring.getMonitoringType(); MonitoringType monitoringType = monitoring.getMonitoringType();
TariffDTO tariffMonitoring = tariffService.getTariffByType(TariffType.valueOf(monitoringType.name())); TariffDTO tariffMonitoring = tariffService.getTariffByType(TariffType.valueOf(monitoringType.name()));
User user = userRepository.findById(monitoring.getUserId()).orElseThrow();
try { try {
tariffInfoService.writeOffTokens(monitoring.getUserId(), tariffMonitoring.getTokens()); tariffInfoService.writeOffTokens(monitoring.getUserId(), tariffMonitoring.getTokens(), OperationType.MONITORING);
boolean useYandex = searchProperties.getEngines().getOrDefault("yandex", boolean useYandex = searchProperties.getEngines().getOrDefault("yandex",
new SearchProperties.EngineConfig()).isEnabled(); new SearchProperties.EngineConfig()).isEnabled();
@@ -88,10 +90,14 @@ public class MonitoringSearchService {
allResults.addAll(yandexImages); allResults.addAll(yandexImages);
log.info("Yandex search found {} results", yandexImages.size()); log.info("Yandex search found {} results", yandexImages.size());
} catch (TimeoutException e) { } catch (TimeoutException | IOException e) {
log.warn("Yandex search timeout for monitoring file {}", monitoring.getFile().getId()); log.error("Error processing monitoring search", e);
} catch (IOException e) { monitoring.setLastRunStatus("ERROR: " + e.getMessage());
log.error("Yandex search failed for monitoring file {}", monitoring.getFile().getId(), e); updateNextRun(monitoring);
monitoringRepository.save(monitoring);
notificationService.addNotification(NotificationType.SEARCH_OPERATION_FAILED, user.getId());
} }
} else { } else {
log.info("Yandex search is disabled by settings"); log.info("Yandex search is disabled by settings");
@@ -108,10 +114,14 @@ public class MonitoringSearchService {
allResults.addAll(googleImages); allResults.addAll(googleImages);
log.info("Google search found {} results", googleImages.size()); log.info("Google search found {} results", googleImages.size());
} catch (TimeoutException e) { } catch (TimeoutException | IOException e) {
log.warn("Google search timeout for monitoring file {}", monitoring.getFile().getId()); log.error("Error processing monitoring search", e);
} catch (IOException e) { monitoring.setLastRunStatus("ERROR: " + e.getMessage());
log.error("Google search failed for monitoring file {}", monitoring.getFile().getId(), e); updateNextRun(monitoring);
monitoringRepository.save(monitoring);
notificationService.addNotification(NotificationType.SEARCH_OPERATION_FAILED, user.getId());
} }
} else { } else {
log.info("Google search is disabled by settings"); log.info("Google search is disabled by settings");
@@ -138,7 +148,6 @@ public class MonitoringSearchService {
} }
} catch (TariffNotFoundException e) { } catch (TariffNotFoundException e) {
User user = userRepository.findById(monitoring.getUserId()).orElseThrow();
TariffInfo activeTariffInfo = user.getActiveTariffInfo(); TariffInfo activeTariffInfo = user.getActiveTariffInfo();
int currentTokens = activeTariffInfo.getTokens() + activeTariffInfo.getBoughtTokens(); int currentTokens = activeTariffInfo.getTokens() + activeTariffInfo.getBoughtTokens();
@@ -153,7 +162,6 @@ public class MonitoringSearchService {
notificationService.addNotification(NotificationType.TOKEN_NOT_FOUND, user.getId(), notificationService.addNotification(NotificationType.TOKEN_NOT_FOUND, user.getId(),
NotificationMessage.FILE_SEARCH.getMessageKey()); NotificationMessage.FILE_SEARCH.getMessageKey());
} catch (Exception e) { } catch (Exception e) {
User user = userRepository.findById(monitoring.getUserId()).orElseThrow();
log.error("Error processing monitoring search", e); log.error("Error processing monitoring search", e);
monitoring.setLastRunStatus("ERROR: " + e.getMessage()); monitoring.setLastRunStatus("ERROR: " + e.getMessage());
updateNextRun(monitoring); updateNextRun(monitoring);
@@ -12,6 +12,7 @@ import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.entity.search.GlobalSearchResult; import ru.soune.nocopy.entity.search.GlobalSearchResult;
import ru.soune.nocopy.entity.search.GlobalSearchTask; import ru.soune.nocopy.entity.search.GlobalSearchTask;
import ru.soune.nocopy.entity.search.SearchStatus; import ru.soune.nocopy.entity.search.SearchStatus;
import ru.soune.nocopy.entity.tokenoperation.OperationType;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.GlobalSearchResultRepository; import ru.soune.nocopy.repository.GlobalSearchResultRepository;
import ru.soune.nocopy.repository.GlobalSearchTaskRepository; import ru.soune.nocopy.repository.GlobalSearchTaskRepository;
@@ -59,7 +60,7 @@ public class GlobalSearchAsyncProcessor {
FileEntity file = fileEntityRepository.findById(uuid) FileEntity file = fileEntityRepository.findById(uuid)
.orElseThrow(() -> new RuntimeException("File not found: " + uuid)); .orElseThrow(() -> new RuntimeException("File not found: " + uuid));
tariffInfoService.writeOffTokens(userId, TariffConstants.TOKEN_VALUE_FOR_SEARCH); tariffInfoService.writeOffTokens(userId, TariffConstants.TOKEN_VALUE_FOR_SEARCH, OperationType.GLOBAL_SEARCH);
GlobalSearchResult result = processFile(file, taskId, userId); GlobalSearchResult result = processFile(file, taskId, userId);
globalSearchResultRepository.save(result); globalSearchResultRepository.save(result);
@@ -0,0 +1,55 @@
package ru.soune.nocopy.service.statistic;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import ru.soune.nocopy.dto.statistic.IncomeStatisticResponse;
import ru.soune.nocopy.repository.ReferralJpaRepository;
import ru.soune.nocopy.util.PercentileCalculator;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Slf4j
public class IncomeStatisticService {
private final ReferralJpaRepository referralRepository;
private static final int PERCENTILE_5 = 5;
private static final int PERCENTILE_50 = 50;
private static final int PERCENTILE_95 = 95;
public IncomeStatisticResponse getIncomeStatistics() {
Long totalIncome = referralRepository.getTotalIncome();
Long totalAvailable = referralRepository.getTotalAvailableIncome();
Long totalHold = referralRepository.getTotalHoldBalance();
List<Object[]> incomeData = referralRepository.getIncomePerUser();
List<Long> incomeValues = extractIncomeValues(incomeData);
PercentileCalculator.PercentilesResult incomePercentiles =
PercentileCalculator.calculatePercentiles(incomeValues, PERCENTILE_5, PERCENTILE_50,
PERCENTILE_95);
return IncomeStatisticResponse.builder()
.totalIncome(totalIncome != null ? totalIncome : 0L)
.totalAvailable(totalAvailable != null ? totalAvailable : 0L)
.totalHold(totalHold != null ? totalHold : 0L)
.incomePerUser(new IncomeStatisticResponse.Percentiles(
incomePercentiles.getP5() != null ? incomePercentiles.getP5() : 0L,
incomePercentiles.getP50() != null ? incomePercentiles.getP50() : 0L,
incomePercentiles.getP95() != null ? incomePercentiles.getP95() : 0L
))
.build();
}
private List<Long> extractIncomeValues(List<Object[]> data) {
return data.stream()
.map(row -> row[1] != null ? ((Number) row[1]).longValue() : 0L)
.filter(value -> value > 0)
.collect(Collectors.toList());
}
}
@@ -0,0 +1,118 @@
package ru.soune.nocopy.service.statistic;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import ru.soune.nocopy.dto.statistic.ProtectedFilesStatisticResponse;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.util.PercentileCalculator;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Slf4j
public class ProtectedFilesStatisticService {
private static final int PERCENTILE_5 = 5;
private static final int PERCENTILE_50 = 50;
private static final int PERCENTILE_95 = 95;
private static final double BYTES_TO_MB = 1048576.0;
private static final double BYTES_TO_GB = 1073741824.0;
private final FileEntityRepository fileEntityRepository;
public ProtectedFilesStatisticResponse getProtectedFilesStatistics() {
List<Object[]> totalStats = fileEntityRepository.getProtectedFilesTotalStats();
Long totalFiles = extractTotalFiles(totalStats);
Long totalSize = extractTotalSize(totalStats);
List<Object[]> userStats = fileEntityRepository.getProtectedFilesPerUserStats();
List<Long> fileCounts = extractFileCounts(userStats);
List<Long> fileSizes = extractFileSizes(userStats);
PercentileCalculator.PercentilesResult filesPercentiles =
PercentileCalculator.calculatePercentiles(fileCounts, PERCENTILE_5, PERCENTILE_50, PERCENTILE_95);
PercentileCalculator.PercentilesResult sizePercentiles =
PercentileCalculator.calculatePercentiles(fileSizes, PERCENTILE_5, PERCENTILE_50, PERCENTILE_95);
return buildResponse(totalFiles, totalSize, filesPercentiles, sizePercentiles);
}
private Long extractTotalFiles(List<Object[]> totalStats) {
if (totalStats.isEmpty() || totalStats.get(0) == null) {
return 0L;
}
Object[] row = totalStats.get(0);
return row[0] != null ? ((Number) row[0]).longValue() : 0L;
}
private Long extractTotalSize(List<Object[]> totalStats) {
if (totalStats.isEmpty() || totalStats.get(0) == null) {
return 0L;
}
Object[] row = totalStats.get(0);
return row[1] != null ? ((Number) row[1]).longValue() : 0L;
}
private List<Long> extractFileCounts(List<Object[]> userStats) {
return userStats.stream()
.map(row -> row[1] != null ? ((Number) row[1]).longValue() : 0L)
.collect(Collectors.toList());
}
private List<Long> extractFileSizes(List<Object[]> userStats) {
return userStats.stream()
.map(row -> row[2] != null ? ((Number) row[2]).longValue() : 0L)
.collect(Collectors.toList());
}
private ProtectedFilesStatisticResponse buildResponse(
Long totalFiles,
Long totalSize,
PercentileCalculator.PercentilesResult filesPercentiles,
PercentileCalculator.PercentilesResult sizePercentiles) {
ProtectedFilesStatisticResponse.Percentiles filesPerUser =
new ProtectedFilesStatisticResponse.Percentiles(
filesPercentiles.getP5(),
filesPercentiles.getP50(),
filesPercentiles.getP95());
ProtectedFilesStatisticResponse.SizePercentiles sizePerUser =
new ProtectedFilesStatisticResponse.SizePercentiles(
sizePercentiles.getP5(),
sizePercentiles.getP50(),
sizePercentiles.getP95(),
convertBytesToMb(sizePercentiles.getP5()),
convertBytesToMb(sizePercentiles.getP50()),
convertBytesToMb(sizePercentiles.getP95()));
return ProtectedFilesStatisticResponse.builder()
.totalFiles(totalFiles)
.totalSizeBytes(totalSize)
.totalSizeGb(convertBytesToGb(totalSize))
.filesPerUser(filesPerUser)
.sizePerUser(sizePerUser)
.build();
}
private Double convertBytesToMb(Long bytes) {
if (bytes == null || bytes == 0) {
return 0.0;
}
return bytes / BYTES_TO_MB;
}
private Double convertBytesToGb(Long bytes) {
if (bytes == null || bytes == 0) {
return 0.0;
}
return bytes / BYTES_TO_GB;
}
}
@@ -0,0 +1,118 @@
package ru.soune.nocopy.service.statistic;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import ru.soune.nocopy.dto.statistic.SubscriberStatisticResponse;
import ru.soune.nocopy.entity.tarif.TariffInfo;
import ru.soune.nocopy.entity.tarif.TariffTimeTerm;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.repository.TariffInfoRepository;
import ru.soune.nocopy.repository.UserRepository;
import java.util.*;
@Service
@RequiredArgsConstructor
@Slf4j
public class SubscriberStatisticService {
private final TariffInfoRepository tariffInfoRepository;
private final UserRepository userRepository;
public SubscriberStatisticResponse getSubscriberStatistics() {
return SubscriberStatisticResponse.builder()
.monthly(buildPeriodStatistics(TariffTimeTerm.MONTHLY))
.yearly(buildPeriodStatistics(TariffTimeTerm.YEAR))
.build();
}
private SubscriberStatisticResponse.PeriodStatistics buildPeriodStatistics(TariffTimeTerm term) {
Long currentActive = tariffInfoRepository.getCurrentActiveSubscriptions(term);
List<TariffInfo> allTariffs = tariffInfoRepository.getAllTariffInfosForAnalysis(term);
Map<Long, List<TariffInfo>> tariffsByUser = groupTariffsByUser(allTariffs);
Long firstTime = countFirstTimeSubscriptions(tariffsByUser);
Long renewals = countRenewals(tariffsByUser);
UpDownGradeResult gradeResult = analyzeUpgradesAndDowngrades(tariffsByUser);
return SubscriberStatisticResponse.PeriodStatistics.builder()
.currentActiveSubscriptions(currentActive != null ? currentActive : 0L)
.firstTimeSubscriptions(firstTime)
.renewals(renewals)
.upgrades(gradeResult.upgrades)
.downgrades(gradeResult.downgrades)
.build();
}
private Map<Long, List<TariffInfo>> groupTariffsByUser(List<TariffInfo> tariffs) {
Map<Long, List<TariffInfo>> result = new HashMap<>();
for (TariffInfo ti : tariffs) {
Optional<User> userOpt = userRepository.findByPersonalTariffInfoId(ti.getId());
if (userOpt.isPresent()) {
Long userId = userOpt.get().getId();
result.computeIfAbsent(userId, k -> new ArrayList<>()).add(ti);
}
}
return result;
}
private Long countFirstTimeSubscriptions(Map<Long, List<TariffInfo>> tariffsByUser) {
return (long) tariffsByUser.size();
}
private Long countRenewals(Map<Long, List<TariffInfo>> tariffsByUser) {
return tariffsByUser.values().stream()
.filter(list -> list.size() > 1)
.mapToLong(list -> list.size() - 1)
.sum();
}
private UpDownGradeResult analyzeUpgradesAndDowngrades(Map<Long, List<TariffInfo>> tariffsByUser) {
long upgrades = 0L;
long downgrades = 0L;
for (List<TariffInfo> userTariffs : tariffsByUser.values()) {
if (userTariffs.size() < 2) {
continue;
}
userTariffs.sort(Comparator.comparing(TariffInfo::getStartTariff,
Comparator.nullsLast(Comparator.naturalOrder())));
for (int i = 1; i < userTariffs.size(); i++) {
TariffInfo previous = userTariffs.get(i - 1);
TariffInfo current = userTariffs.get(i);
if (previous.getTariff() == null || current.getTariff() == null) {
continue;
}
double previousPrice = previous.getTariff().getPrice();
double currentPrice = current.getTariff().getPrice();
if (currentPrice > previousPrice) {
upgrades++;
} else if (currentPrice < previousPrice) {
downgrades++;
}
}
}
return new UpDownGradeResult(upgrades, downgrades);
}
private static class UpDownGradeResult {
final Long upgrades;
final Long downgrades;
UpDownGradeResult(Long upgrades, Long downgrades) {
this.upgrades = upgrades;
this.downgrades = downgrades;
}
}
}
@@ -0,0 +1,65 @@
package ru.soune.nocopy.service.statistic;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import ru.soune.nocopy.dto.statistic.TokenStatisticResponse;
import ru.soune.nocopy.repository.PaymentRepository;
import ru.soune.nocopy.repository.TokenOperationRepository;
import ru.soune.nocopy.util.PercentileCalculator;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Slf4j
public class TokenStatisticService {
private final PaymentRepository paymentRepository;
private final TokenOperationRepository tokenOperationRepository;
private static final int PERCENTILE_5 = 5;
private static final int PERCENTILE_50 = 50;
private static final int PERCENTILE_95 = 95;
public TokenStatisticResponse getTokenStatistics() {
Long totalBought = paymentRepository.getTotalTokensBought();
Long totalSpent = tokenOperationRepository.getTotalTokensSpent();
List<Object[]> boughtData = paymentRepository.getTokensBoughtPerUser();
List<Long> boughtValues = extractTokenValues(boughtData);
List<Object[]> spentData = tokenOperationRepository.getTokensSpentPerUser();
List<Long> spentValues = extractTokenValues(spentData);
PercentileCalculator.PercentilesResult boughtPercentiles =
PercentileCalculator.calculatePercentiles(boughtValues, PERCENTILE_5, PERCENTILE_50, PERCENTILE_95);
PercentileCalculator.PercentilesResult spentPercentiles =
PercentileCalculator.calculatePercentiles(spentValues, PERCENTILE_5, PERCENTILE_50, PERCENTILE_95);
return TokenStatisticResponse.builder()
.totalBought(totalBought != null ? totalBought : 0L)
.totalSpent(totalSpent != null ? totalSpent : 0L)
.boughtPerUser(new TokenStatisticResponse.Percentiles(
boughtPercentiles.getP5(),
boughtPercentiles.getP50(),
boughtPercentiles.getP95()
))
.spentPerUser(new TokenStatisticResponse.Percentiles(
spentPercentiles.getP5(),
spentPercentiles.getP50(),
spentPercentiles.getP95()
))
.build();
}
private List<Long> extractTokenValues(List<Object[]> data) {
return data.stream()
.map(row -> row[1] != null ? ((Number) row[1]).longValue() : 0L)
.filter(value -> value > 0)
.collect(Collectors.toList());
}
}
@@ -0,0 +1,46 @@
package ru.soune.nocopy.service.statistic;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import ru.soune.nocopy.dto.statistic.UserDynamicStatisticResponse;
import ru.soune.nocopy.repository.UserRepository;
@Service
@RequiredArgsConstructor
@Slf4j
public class UserDynamicStatisticService {
private final UserRepository userRepository;
public UserDynamicStatisticResponse getUserDynamicStatistics() {
Long totalUsers = userRepository.getTotalUsersCount();
Long newLast30Days = userRepository.getNewUsersCount(30);
Long totalUpTo60Days = userRepository.getNewUsersCount(60);
Long newPrevious30Days = totalUpTo60Days - newLast30Days;
Double growthPercent = calculateGrowthPercent(newLast30Days, newPrevious30Days);
Long newCurrentMonth = userRepository.getNewUsersCurrentMonth();
Long newPreviousMonth = userRepository.getNewUsersPreviousMonth();
Double monthGrowthPercent = calculateGrowthPercent(newCurrentMonth, newPreviousMonth);
return UserDynamicStatisticResponse.builder()
.totalUsers(totalUsers != null ? totalUsers : 0L)
.newLast30Days(newLast30Days != null ? newLast30Days : 0L)
.newPrevious30Days(newPrevious30Days != null ? newPrevious30Days : 0L)
.growthPercent(growthPercent)
.newCurrentMonth(newCurrentMonth != null ? newCurrentMonth : 0L)
.newPreviousMonth(newPreviousMonth != null ? newPreviousMonth : 0L)
.monthGrowthPercent(monthGrowthPercent)
.build();
}
private Double calculateGrowthPercent(Long current, Long previous) {
if (previous == null || previous == 0) {
return current != null && current > 0 ? 100.0 : 0.0;
}
double percent = ((current - previous) * 100.0) / previous;
return Math.round(percent * 100.0) / 100.0;
}
}
@@ -0,0 +1,40 @@
package ru.soune.nocopy.service.statistic;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import ru.soune.nocopy.dto.statistic.ViolationStatisticResponse;
import ru.soune.nocopy.repository.ComplaintEntityRepository;
import ru.soune.nocopy.repository.LawCaseRepository;
import ru.soune.nocopy.repository.ViolationRepository;
@Service
@RequiredArgsConstructor
@Slf4j
public class ViolationStatisticService {
private final ViolationRepository violationRepository;
private final ComplaintEntityRepository complaintRepository;
private final LawCaseRepository lawCaseRepository;
public ViolationStatisticResponse getViolationStatistics() {
Long totalViolations = violationRepository.getTotalViolationsCount();
Long totalComplaints = complaintRepository.getTotalComplaintsCount();
Long totalLawCases = lawCaseRepository.getTotalLawCasesCount();
Long complaintsWithoutCase = complaintRepository.getComplaintsWithoutLawCaseCount();
Long violationsWithComplaint = violationRepository.getViolationsWithComplaintCount();
Long violationsWithoutComplaint = totalViolations - violationsWithComplaint;
return ViolationStatisticResponse.builder()
.totalViolations(totalViolations != null ? totalViolations : 0L)
.totalComplaints(totalComplaints != null ? totalComplaints : 0L)
.totalLawCases(totalLawCases != null ? totalLawCases : 0L)
.complaintsWithoutCase(complaintsWithoutCase != null ? complaintsWithoutCase : 0L)
.violationsWithComplaint(violationsWithComplaint != null ? violationsWithComplaint : 0L)
.violationsWithoutComplaint(violationsWithoutComplaint)
.build();
}
}
@@ -10,12 +10,14 @@ import ru.soune.nocopy.entity.tarif.Tariff;
import ru.soune.nocopy.entity.tarif.TariffInfo; import ru.soune.nocopy.entity.tarif.TariffInfo;
import ru.soune.nocopy.entity.tarif.TariffStatus; import ru.soune.nocopy.entity.tarif.TariffStatus;
import ru.soune.nocopy.entity.tarif.TariffType; import ru.soune.nocopy.entity.tarif.TariffType;
import ru.soune.nocopy.entity.tokenoperation.OperationType;
import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.TariffNotFoundException; import ru.soune.nocopy.exception.TariffNotFoundException;
import ru.soune.nocopy.repository.TariffInfoRepository; import ru.soune.nocopy.repository.TariffInfoRepository;
import ru.soune.nocopy.repository.TariffRepository; import ru.soune.nocopy.repository.TariffRepository;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.mail.EmailService; import ru.soune.nocopy.service.mail.EmailService;
import ru.soune.nocopy.service.tokenoperation.TokenOperationService;
import java.io.IOException; import java.io.IOException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -35,6 +37,8 @@ public class TariffInfoService {
private EmailService emailService; private EmailService emailService;
private TokenOperationService tokenOperationService;
public TariffInfo createTariffInfo(LocalDateTime startTariff, LocalDateTime endTariff, TariffStatus tariffStatus, public TariffInfo createTariffInfo(LocalDateTime startTariff, LocalDateTime endTariff, TariffStatus tariffStatus,
Integer tokens) { Integer tokens) {
TariffInfo tariffInfo = new TariffInfo(); TariffInfo tariffInfo = new TariffInfo();
@@ -48,7 +52,7 @@ public class TariffInfoService {
} }
// @Transactional // @Transactional
public void writeOffTokens(long userId, Integer value) throws MessagingException, IOException { public void writeOffTokens(long userId, Integer value, OperationType operationType) throws MessagingException, IOException {
User user = userRepository.findById(userId).orElseThrow(); User user = userRepository.findById(userId).orElseThrow();
TariffInfo activeTariffInfo; TariffInfo activeTariffInfo;
@@ -60,6 +64,8 @@ public class TariffInfoService {
} }
writeOffTokensForTariff(activeTariffInfo, value); writeOffTokensForTariff(activeTariffInfo, value);
tokenOperationService.create(operationType, userId, Long.valueOf(value));
} }
public void writeOffTokensForTariff(TariffInfo activeTariffInfo, Integer value) throws MessagingException, IOException { public void writeOffTokensForTariff(TariffInfo activeTariffInfo, Integer value) throws MessagingException, IOException {
@@ -0,0 +1,122 @@
package ru.soune.nocopy.service.tokenoperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.entity.tokenoperation.OperationType;
import ru.soune.nocopy.entity.tokenoperation.TokenOperation;
import ru.soune.nocopy.repository.TokenOperationRepository;
import jakarta.persistence.EntityNotFoundException;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class TokenOperationService {
private final TokenOperationRepository repository;
@Transactional
public TokenOperation create(OperationType operationType, Long userId, Long spent) {
TokenOperation tokenOperation = new TokenOperation();
tokenOperation.setOperationType(operationType);
tokenOperation.setUserId(userId);
tokenOperation.setSpent(spent);
tokenOperation.setCreatedAt(LocalDateTime.now());
return repository.save(tokenOperation);
}
public TokenOperation findById(Long id) {
log.debug("Finding token operation by id: {}", id);
return repository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("TokenOperation not found with id: " + id));
}
public Page<TokenOperation> findAll(int page, int size, String sortBy, String direction) {
log.debug("Finding all operations with pagination: page={}, size={}, sortBy={}, dir={}",
page, size, sortBy, direction);
Sort sort = direction.equalsIgnoreCase("desc")
? Sort.by(sortBy).descending()
: Sort.by(sortBy).ascending();
Pageable pageable = PageRequest.of(page, size, sort);
return repository.findAll(pageable);
}
public Page<TokenOperation> findByFilters(
Long userId,
OperationType operationType,
Long minSpent,
Long maxSpent,
int page,
int size,
String sortBy,
String direction) {
log.debug("Finding by filters: userId={}, type={}, minSpent={}, maxSpent={}",
userId, operationType, minSpent, maxSpent);
Sort sort = Sort.by(Sort.Direction.fromString(direction), sortBy);
Pageable pageable = PageRequest.of(page, size, sort);
return repository.findByFilters(userId, operationType, minSpent, maxSpent, pageable);
}
public Page<TokenOperation> findByUserId(Long userId, int page, int size) {
log.debug("Finding operations for user id: {}", userId);
Pageable pageable = PageRequest.of(page, size, Sort.by("id").descending());
return repository.findByUserId(userId, pageable);
}
@Transactional
public TokenOperation update(Long id, TokenOperation updatedOperation) {
log.info("Updating token operation with id: {}", id);
TokenOperation existing = findById(id);
existing.setOperationType(updatedOperation.getOperationType());
existing.setUserId(updatedOperation.getUserId());
existing.setSpent(updatedOperation.getSpent());
return repository.save(existing);
}
@Transactional
public void deleteById(Long id) {
log.info("Deleting token operation with id: {}", id);
if (!repository.existsById(id)) {
throw new EntityNotFoundException("TokenOperation not found with id: " + id);
}
repository.deleteById(id);
}
@Transactional
public void deleteByUserId(Long userId) {
log.warn("Deleting all operations for user id: {}", userId);
List<TokenOperation> userOps = repository.findByUserId(userId);
repository.deleteAll(userOps);
}
public Long getTotalSpentByUserAndType(Long userId, OperationType type) {
Long sum = repository.sumSpentByUserAndType(userId, type);
return sum != null ? sum : 0L;
}
public Long countByUserId(Long userId) {
return repository.countByUserId(userId);
}
}
@@ -8,6 +8,7 @@ import ru.soune.nocopy.entity.user.moderation.UserVerification;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.repository.UserVerificationRepository; import ru.soune.nocopy.repository.UserVerificationRepository;
import java.time.LocalDateTime;
import java.util.List; import java.util.List;
@Service @Service
@@ -25,13 +26,15 @@ public class UserVerificationService {
UserVerification userVerification = UserVerification.builder() UserVerification userVerification = UserVerification.builder()
.moderationStatus(ModerationStatus.VERIFICATION_IN_PROGRESS) .moderationStatus(ModerationStatus.VERIFICATION_IN_PROGRESS)
.userId(user.getId())
.createdAt(LocalDateTime.now())
.build(); .build();
userVerificationRepository.save(userVerification);
user.setVerificationStatus(ModerationStatus.VERIFICATION_IN_PROGRESS); user.setVerificationStatus(ModerationStatus.VERIFICATION_IN_PROGRESS);
userRepository.save(user); userRepository.save(user);
userVerificationRepository.save(userVerification);
} }
public UserVerification verifyUser(Long userId, String message, Long adminId, boolean verified) { public UserVerification verifyUser(Long userId, String message, Long adminId, boolean verified) {
@@ -191,7 +191,6 @@ public class ViolationService {
} }
Set<String> inProgressStatuses = Set.of( Set<String> inProgressStatuses = Set.of(
ViolationStatus.SHOWED.name(),
ViolationStatus.LEGAL_IN_WORK.name(), ViolationStatus.LEGAL_IN_WORK.name(),
ViolationStatus.COMPLAINT_IN_WORK.name(), ViolationStatus.COMPLAINT_IN_WORK.name(),
ViolationStatus.COMPLAINT_AND_LEGAL_IN_WORK.name() ViolationStatus.COMPLAINT_AND_LEGAL_IN_WORK.name()
@@ -0,0 +1,80 @@
package ru.soune.nocopy.util;
import lombok.experimental.UtilityClass;
import java.util.List;
import java.util.stream.Collectors;
@UtilityClass
public class PercentileCalculator {
/**
* Рассчитать несколько перцентилей за один проход
*
* @param values список значений
* @param percentiles массив перцентилей
* @return PercentilesResult с соответствующими значениями
*/
public PercentilesResult calculatePercentiles(List<Long> values, int... percentiles) {
if (values == null || values.isEmpty()) {
return new PercentilesResult(0L, 0L, 0L);
}
List<Long> sorted = values.stream()
.sorted()
.collect(Collectors.toList());
Long p5 = percentiles.length > 0 ? calculatePercentileFromSorted(sorted, percentiles[0]) : 0L;
Long p50 = percentiles.length > 1 ? calculatePercentileFromSorted(sorted, percentiles[1]) : 0L;
Long p95 = percentiles.length > 2 ? calculatePercentileFromSorted(sorted, percentiles[2]) : 0L;
return new PercentilesResult(p5, p50, p95);
}
public PercentilesResult calculateP5P50P95(List<Long> values) {
return calculatePercentiles(values, 5, 50, 95);
}
private Long calculatePercentileFromSorted(List<Long> sorted, double percentile) {
if (sorted.isEmpty()) {
return 0L;
}
if (percentile <= 0) {
return sorted.get(0);
}
if (percentile >= 100) {
return sorted.get(sorted.size() - 1);
}
double index = (percentile / 100.0) * (sorted.size() - 1);
int lowerIndex = (int) Math.floor(index);
int upperIndex = (int) Math.ceil(index);
if (lowerIndex == upperIndex) {
return sorted.get(lowerIndex);
}
double weight = index - lowerIndex;
long lowerValue = sorted.get(lowerIndex);
long upperValue = sorted.get(upperIndex);
return lowerValue + Math.round(weight * (upperValue - lowerValue));
}
public static class PercentilesResult {
private final Long p5;
private final Long p50;
private final Long p95;
public PercentilesResult(Long p5, Long p50, Long p95) {
this.p5 = p5;
this.p50 = p50;
this.p95 = p95;
}
public Long getP5() { return p5; }
public Long getP50() { return p50; }
public Long getP95() { return p95; }
}
}
+7
View File
@@ -102,6 +102,13 @@ yandex:
api-key: ${YANDEX_API_KEY:AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q} api-key: ${YANDEX_API_KEY:AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q}
folder-id: ${YANDEX_FOLDER_ID:b1gokpdbm6qfpsou8pcd} folder-id: ${YANDEX_FOLDER_ID:b1gokpdbm6qfpsou8pcd}
search-url: ${YANDEX_SEARCH_URL:https://searchapi.api.cloud.yandex.net/v2/image/search_by_image} search-url: ${YANDEX_SEARCH_URL:https://searchapi.api.cloud.yandex.net/v2/image/search_by_image}
cloud:
key: "YCAJEmHZcWxzf4oGWvETG3mms"
secret-key: "YCPAPZ32XC-LWb8xE0b_xTcBD8NZUCHpSOfu8LUG"
region: "ru-central1"
s3-endpoint: "https://storage.yandexcloud.net"
bucket: "no-copy-storage"
searchapi: searchapi:
api-key: ${SEARCHAPI_API_KEY:5jyYZC8jSaxhZTwjMUhwtAXi} api-key: ${SEARCHAPI_API_KEY:5jyYZC8jSaxhZTwjMUhwtAXi}
Binary file not shown.