From 70f68d40468c6bd7761fdddb18449a1c2f615c0d Mon Sep 17 00:00:00 2001 From: Vladimir Voronin Date: Thu, 23 Jan 2025 00:16:17 +0300 Subject: [PATCH] + analytics --- .../controllers/CategoriesController.kt | 6 + .../budgerapp/services/CategoryService.kt | 193 +++++++++++++++++- 2 files changed, 198 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/space/luminic/budgerapp/controllers/CategoriesController.kt b/src/main/kotlin/space/luminic/budgerapp/controllers/CategoriesController.kt index 7de8a32..c014b36 100644 --- a/src/main/kotlin/space/luminic/budgerapp/controllers/CategoriesController.kt +++ b/src/main/kotlin/space/luminic/budgerapp/controllers/CategoriesController.kt @@ -81,4 +81,10 @@ class CategoriesController( return categoryService.getCategorySumsPipeline(LocalDate.of(2024, 8, 1), LocalDate.of(2025, 1, 12)) } + @GetMapping("/by-month2") + fun getCategoriesSumsByMonthsV2(): Mono> { + return categoryService.getCategorySummaries(LocalDate.now().minusMonths(6)) + } + + } \ No newline at end of file diff --git a/src/main/kotlin/space/luminic/budgerapp/services/CategoryService.kt b/src/main/kotlin/space/luminic/budgerapp/services/CategoryService.kt index 0909b95..b6d35d2 100644 --- a/src/main/kotlin/space/luminic/budgerapp/services/CategoryService.kt +++ b/src/main/kotlin/space/luminic/budgerapp/services/CategoryService.kt @@ -20,6 +20,7 @@ import java.time.LocalDateTime import java.time.LocalTime import java.time.ZoneId import java.time.ZoneOffset +import java.util.Calendar import java.util.Date @Service @@ -376,7 +377,7 @@ class CategoryService( "date", Document( "\$gte", Date.from( - LocalDateTime.of(dateTo, LocalTime.MIN) + LocalDateTime.of(dateFrom, LocalTime.MIN) .atZone(ZoneId.systemDefault()) .withZoneSameInstant(ZoneOffset.UTC).toInstant() ) @@ -545,5 +546,195 @@ class CategoryService( .collectList() } + fun getCategorySummaries(dateFrom: LocalDate): Mono> { + val sixMonthsAgo = Date.from( + LocalDateTime.of(dateFrom, LocalTime.MIN) + .atZone(ZoneId.systemDefault()) + .withZoneSameInstant(ZoneOffset.UTC).toInstant() + ) // Пример даты, можно заменить на вычисляемую + + val aggregation = listOf( + // 1. Фильтр за последние 6 месяцев + Document( + "\$match", + Document("date", Document("\$gte", sixMonthsAgo).append("\$lt", Date())).append("type.code", "INSTANT") + ), + + // 2. Группируем по категории + (год, месяц) + Document( + "\$group", Document( + "_id", Document("category", "\$category.\$id") + .append("year", Document("\$year", "\$date")) + .append("month", Document("\$month", "\$date")) + ) + .append("totalAmount", Document("\$sum", "\$amount")) + ), + + // 3. Подтягиваем информацию о категории + Document( + "\$lookup", Document("from", "categories") + .append("localField", "_id.category") + .append("foreignField", "_id") + .append("as", "categoryInfo") + ), + + // 4. Распаковываем массив категорий + Document("\$unwind", "\$categoryInfo"), + + // 5. Фильтруем по типу категории (EXPENSE) + Document("\$match", Document("categoryInfo.type.code", "EXPENSE")), + + // 6. Группируем обратно по категории, собирая все (год, месяц, total) + Document( + "\$group", Document("_id", "\$_id.category") + .append("categoryName", Document("\$first", "\$categoryInfo.name")) + .append("categoryType", Document("\$first", "\$categoryInfo.type.code")) + .append("categoryIcon", Document("\$first", "\$categoryInfo.icon")) + .append( + "monthlySums", Document( + "\$push", Document("year", "\$_id.year") + .append("month", "\$_id.month") + .append("total", "\$totalAmount") + ) + ) + ), + + // 7. Формируем единый массив из 6 элементов: + // - каждый элемент = {year, month, total}, + // - если нет записей за месяц, ставим total=0 + Document( + "\$project", Document("categoryName", 1) + .append("categoryType", 1) + .append("categoryIcon", 1) + .append( + "monthlySums", Document( + "\$map", Document("input", Document("\$range", listOf(0, 6))) + .append("as", "i") + .append( + "in", Document( + "\$let", Document( + "vars", Document( + "subDate", Document( + "\$dateSubtract", Document("startDate", Date()) + .append("unit", "month") + .append("amount", "$\$i") + ) + ) + ) + .append( + "in", Document("year", Document("\$year", "$\$subDate")) + .append("month", Document("\$month", "$\$subDate")) + .append( + "total", Document( + "\$ifNull", listOf( + Document( + "\$getField", Document("field", "total") + .append( + "input", Document( + "\$arrayElemAt", listOf( + Document( + "\$filter", + Document( + "input", + "\$monthlySums" + ) + .append("as", "ms") + .append( + "cond", Document( + "\$and", listOf( + Document( + "\$eq", + listOf( + "$\$ms.year", + Document( + "\$year", + "$\$subDate" + ) + ) + ), + Document( + "\$eq", + listOf( + "$\$ms.month", + Document( + "\$month", + "$\$subDate" + ) + ) + ) + ) + ) + ) + ), 0.0 + ) + ) + ) + ), 0.0 + ) + ) + ) + ) + ) + ) + ) + ) + ), + + // 8. Сортируем результат по имени категории + Document("\$sort", Document("categoryName", 1)) + ) + + // Выполняем агрегацию + return mongoTemplate.getCollection("transactions") + .flatMapMany { it.aggregate(aggregation) } + .map { document -> + // Преобразуем _id в строку + document["_id"] = document["_id"].toString() + + // Получаем monthlySums и приводим к изменяемому списку + val monthlySums = (document["monthlySums"] as? List<*>)?.map { monthlySum -> + if (monthlySum is Document) { + // Создаем копию Document, чтобы избежать изменений в исходном списке + Document(monthlySum).apply { + // Добавляем поле date + val date = LocalDate.of(getInteger("year"), getInteger("month"), 1) + this["date"] = date + } + } else { + monthlySum + } + }?.toMutableList() + + // Сортируем monthlySums по полю date + val sortedMonthlySums = monthlySums?.sortedBy { (it as? Document)?.get("date") as? LocalDate } + + // Рассчитываем разницу между текущим и предыдущим месяцем + var previousMonthSum = 0.0 + sortedMonthlySums?.forEach { monthlySum -> + if (monthlySum is Document) { + val currentMonthSum = monthlySum.getDouble("total") ?: 0.0 + + // Рассчитываем разницу в процентах + val difference = if (previousMonthSum != 0.0 && currentMonthSum != 0.0) { + (((currentMonthSum - previousMonthSum) / previousMonthSum) * 100).toInt() + } else { + 0 + } + + // Добавляем поле difference + monthlySum["difference"] = difference + + // Обновляем previousMonthSum для следующей итерации + previousMonthSum = currentMonthSum + } + } + + // Обновляем документ с отсортированными и обновленными monthlySums + document["monthlySums"] = sortedMonthlySums + document + } + .collectList() + } + }