This commit is contained in:
xds
2025-04-07 18:26:23 +03:00
parent a38d5068e0
commit af7577c65b
13 changed files with 229 additions and 62 deletions

View File

@@ -58,6 +58,7 @@ dependencies {
implementation("org.telegram:telegrambots:6.9.7.1") implementation("org.telegram:telegrambots:6.9.7.1")
implementation("org.telegram:telegrambots-spring-boot-starter:6.9.7.1") implementation("org.telegram:telegrambots-spring-boot-starter:6.9.7.1")
implementation("com.opencsv:opencsv:5.10")

View File

@@ -2,12 +2,14 @@ package space.luminic.budgerapp
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.runApplication import org.springframework.boot.runApplication
import org.springframework.cache.annotation.EnableCaching import org.springframework.cache.annotation.EnableCaching
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories import org.springframework.data.mongodb.repository.config.EnableMongoRepositories
import org.springframework.scheduling.annotation.EnableAsync import org.springframework.scheduling.annotation.EnableAsync
import org.springframework.scheduling.annotation.EnableScheduling import org.springframework.scheduling.annotation.EnableScheduling
import space.luminic.budgerapp.configs.NLPConfig
import space.luminic.budgerapp.configs.TelegramBotProperties import space.luminic.budgerapp.configs.TelegramBotProperties
import java.util.* import java.util.*
@@ -15,7 +17,8 @@ import java.util.*
@EnableCaching @EnableCaching
@EnableAsync @EnableAsync
@EnableScheduling @EnableScheduling
@EnableConfigurationProperties(TelegramBotProperties::class) //@EnableConfigurationProperties([TelegramBotProperties::class,)
@ConfigurationPropertiesScan(basePackages = ["space.luminic.budgerapp"])
@EnableMongoRepositories(basePackages = ["space.luminic.budgerapp.repos"]) @EnableMongoRepositories(basePackages = ["space.luminic.budgerapp.repos"])
class BudgerAppApplication class BudgerAppApplication

View File

@@ -1,12 +1,19 @@
package space.luminic.budgerapp.configs package space.luminic.budgerapp.configs
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
//@Configuration
//class CommonConfig { //class CommonConfig {
// @Bean // @Bean
// fun httpTraceRepository(): HttpTraceRepository { // fun httpTraceRepository(): HttpTraceRepository {
// return InMemoryHttpTraceRepository() // return InMemoryHttpTraceRepository()
// } // }
//} //}
@ConfigurationProperties(prefix = "nlp")
data class NLPConfig(
val address: String,
)

View File

@@ -1,18 +1,25 @@
package space.luminic.budgerapp.controllers package space.luminic.budgerapp.controllers
import com.opencsv.CSVWriter
import kotlinx.coroutines.reactor.awaitSingle import kotlinx.coroutines.reactor.awaitSingle
import kotlinx.coroutines.reactor.awaitSingleOrNull import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.apache.commons.io.IOUtils.writer
import org.bson.Document import org.bson.Document
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import space.luminic.budgerapp.controllers.BudgetController.LimitValue import space.luminic.budgerapp.controllers.BudgetController.LimitValue
import space.luminic.budgerapp.controllers.dtos.BudgetCreationDTO import space.luminic.budgerapp.controllers.dtos.BudgetCreationDTO
import space.luminic.budgerapp.models.* import space.luminic.budgerapp.models.*
import space.luminic.budgerapp.services.* import space.luminic.budgerapp.services.*
import java.io.ByteArrayOutputStream
import java.io.OutputStreamWriter
import java.time.LocalDate import java.time.LocalDate
@RestController @RestController
@RequestMapping("/spaces") @RequestMapping("/spaces")
class SpaceController( class SpaceController(
@@ -172,6 +179,63 @@ class SpaceController(
} }
} }
@GetMapping("/{spaceId}/transactions/csv")
suspend fun getTransactionsCSV(
@PathVariable spaceId: String,
@RequestParam(value = "transaction_type") transactionType: String? = null,
@RequestParam(value = "category_type") categoryType: String? = null,
@RequestParam(value = "user_id") userId: String? = null,
@RequestParam(value = "is_child") isChild: Boolean? = null,
@RequestParam(value = "limit") limit: Int = 20000,
@RequestParam(value = "offset") offset: Int = 0
): ResponseEntity<Any> {
try {
val bos = ByteArrayOutputStream()
val writer = CSVWriter(OutputStreamWriter(bos))
val CSVHeaders = arrayOf("id", "name", "category")
writer.writeNext(CSVHeaders)
financialService.getTransactions(
spaceId = spaceId,
transactionType = transactionType,
categoryType = categoryType,
userId = userId,
isChild = isChild,
limit = limit,
offset = offset
).awaitSingle().map {
val data = arrayOf(it.id, it.comment, it.category.name)
writer.writeNext(data)
}
writer.close()
val csvData = bos.toByteArray()
val headers = HttpHeaders()
headers.contentType = MediaType.parseMediaType("text/csv")
headers.setContentDispositionFormData("attachment", "pojos.csv")
return ResponseEntity(csvData, headers, HttpStatus.OK)
} catch (e: Exception) {
e.printStackTrace()
return ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR)
}
}
@GetMapping("/{spaceId}/category-predict")
suspend fun getTransactionCategoryPredict(
@PathVariable spaceId: String,
@RequestParam comment: String
): List<Category> {
val user = authService.getSecurityUser()
spaceService.isValidRequest(spaceId, user)
return categoryService.getCategories(
"67af3c0f652da946a7dd9931",
"EXPENSE",
sortBy = "name",
direction = "ASC",
predict = comment
)
}
@GetMapping("/{spaceId}/transactions/{id}") @GetMapping("/{spaceId}/transactions/{id}")
suspend fun getTransaction( suspend fun getTransaction(
@@ -181,6 +245,7 @@ class SpaceController(
return financialService.getTransactionById(id) return financialService.getTransactionById(id)
} }
@PostMapping("/{spaceId}/transactions") @PostMapping("/{spaceId}/transactions")
suspend fun createTransaction(@PathVariable spaceId: String, @RequestBody transaction: Transaction): Transaction { suspend fun createTransaction(@PathVariable spaceId: String, @RequestBody transaction: Transaction): Transaction {
val user = authService.getSecurityUser() val user = authService.getSecurityUser()
@@ -220,7 +285,7 @@ class SpaceController(
): List<Category> { ): List<Category> {
val user = authService.getSecurityUser() val user = authService.getSecurityUser()
spaceService.isValidRequest(spaceId, user) spaceService.isValidRequest(spaceId, user)
return categoryService.getCategories(spaceId, type, sortBy, direction).awaitSingleOrNull().orEmpty() return categoryService.getCategories(spaceId, type, sortBy, direction)
} }
@GetMapping("/{spaceId}/categories/types") @GetMapping("/{spaceId}/categories/types")

View File

@@ -0,0 +1,4 @@
package space.luminic.budgerapp.models
data class CategoryPrediction(val category: String, val weight: Double) {
}

View File

@@ -14,6 +14,7 @@ import org.springframework.stereotype.Service
import org.telegram.telegrambots.bots.TelegramLongPollingBot import org.telegram.telegrambots.bots.TelegramLongPollingBot
import org.telegram.telegrambots.meta.api.methods.send.SendMessage import org.telegram.telegrambots.meta.api.methods.send.SendMessage
import org.telegram.telegrambots.meta.api.methods.updatingmessages.DeleteMessage import org.telegram.telegrambots.meta.api.methods.updatingmessages.DeleteMessage
import org.telegram.telegrambots.meta.api.methods.updatingmessages.EditMessageReplyMarkup
import org.telegram.telegrambots.meta.api.methods.updatingmessages.EditMessageText import org.telegram.telegrambots.meta.api.methods.updatingmessages.EditMessageText
import org.telegram.telegrambots.meta.api.objects.Update import org.telegram.telegrambots.meta.api.objects.Update
import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup
@@ -35,9 +36,76 @@ class BotService(
private val categoriesService: CategoryService, private val categoriesService: CategoryService,
private val financialService: FinancialService, private val financialService: FinancialService,
private val spaceService: SpaceService, private val spaceService: SpaceService,
private val nlpService: NLPService
) : TelegramLongPollingBot(telegramBotProperties.token) { ) : TelegramLongPollingBot(telegramBotProperties.token) {
private val logger = LoggerFactory.getLogger(javaClass) private val logger = LoggerFactory.getLogger(javaClass)
private suspend fun constructCategoriesButtons(
nlp: Boolean,
text: String? = null,
chatId: Long
): InlineKeyboardMarkup {
val categories =
categoriesService.getCategories(
"67af3c0f652da946a7dd9931",
"EXPENSE",
sortBy = "name",
direction = "ASC"
)
val keyboard = InlineKeyboardMarkup()
val buttonLines = mutableListOf<MutableList<InlineKeyboardButton>>()
val filteredCategories = mutableListOf<Category>()
if (nlp) {
if (text.isNullOrBlank()) {
throw TelegramBotException("Текст не может быть пустым", chatId)
}
val predictedCategories = nlpService.predictCategory(text, 0)
for (category in predictedCategories) {
filteredCategories.add(categories.first { it.name == category.category })
}
} else {
filteredCategories.addAll(categories)
}
filteredCategories.map { category ->
val btn = InlineKeyboardButton.builder().text("${category.icon} ${category.name}")
.callbackData("category_${category.id}").build()
if (category.name.length >= 15) {
// Если текст длинный, создаём отдельную строку для кнопки
buttonLines.add(mutableListOf(btn))
} else {
var isAdded = false
// Пытаемся добавить кнопку в существующую строку
for (line in buttonLines) {
if (line.size < 2 && (line.isEmpty() || line[0].text.length < 14)) {
line.add(btn)
isAdded = true
break
}
}
// Если не нашли подходящую строку, создаём новую
if (!isAdded) {
buttonLines.add(mutableListOf(btn))
} else {
}
}
}
val allCatsBtn = InlineKeyboardButton.builder().text("Все категории").callbackData("all_cats").build()
val backButton = InlineKeyboardButton.builder().text("Отмена").callbackData("cancel").build()
buttonLines.add(mutableListOf(allCatsBtn))
buttonLines.add(mutableListOf(backButton))
keyboard.keyboard = buttonLines
return keyboard
}
override fun getBotUsername(): String { override fun getBotUsername(): String {
return telegramBotProperties.username return telegramBotProperties.username
} }
@@ -85,6 +153,12 @@ class BotService(
val deleteMsg = DeleteMessage(chatId, update.callbackQuery.message.messageId) val deleteMsg = DeleteMessage(chatId, update.callbackQuery.message.messageId)
execute(deleteMsg) execute(deleteMsg)
sendMessage(chatId, "Введите сумму и комментарий когда будете готовы.") sendMessage(chatId, "Введите сумму и комментарий когда будете готовы.")
} else if (update.callbackQuery.data == "all_cats") {
val editMessageReplyMarkup = EditMessageReplyMarkup()
editMessageReplyMarkup.chatId = chatId
editMessageReplyMarkup.messageId = update.callbackQuery.message.messageId
editMessageReplyMarkup.replyMarkup = constructCategoriesButtons(false, chatId = chatId.toLong())
execute(editMessageReplyMarkup)
} }
} }
} }
@@ -149,53 +223,12 @@ class BotService(
comment += "$word " comment += "$word "
} }
val categories =
categoriesService.getCategories(
"67af3c0f652da946a7dd9931",
"EXPENSE",
sortBy = "name",
direction = "ASC"
)
.awaitSingle()
val keyboard = InlineKeyboardMarkup()
val buttonLines = mutableListOf<MutableList<InlineKeyboardButton>>()
categories.map { category ->
val btn = InlineKeyboardButton.builder().text("${category.icon} ${category.name}")
.callbackData("category_${category.id}").build()
if (category.name.length >= 15) {
// Если текст длинный, создаём отдельную строку для кнопки
buttonLines.add(mutableListOf(btn))
} else {
var isAdded = false
// Пытаемся добавить кнопку в существующую строку
for (line in buttonLines) {
if (line.size < 2 && (line.isEmpty() || line[0].text.length < 14)) {
line.add(btn)
isAdded = true
break
}
}
// Если не нашли подходящую строку, создаём новую
if (!isAdded) {
buttonLines.add(mutableListOf(btn))
} else {
}
}
}
val backButton = InlineKeyboardButton.builder().text("Отмена").callbackData("cancel").build()
buttonLines.add(mutableListOf(backButton))
keyboard.keyboard = buttonLines
val message = SendMessage() val message = SendMessage()
message.chatId = chatId message.chatId = chatId
val msg = "Выберите категорию" val msg = "Выберите категорию"
message.text = msg message.text = msg
message.replyMarkup = keyboard message.replyMarkup = constructCategoriesButtons(true, text, chatId.toLong())
val userState = BotUserState(user = user) val userState = BotUserState(user = user)
val chatData = val chatData =
userState.data.find { it.chatId == chatId } ?: ChatData(chatId, state = BotStates.WAIT_CATEGORY) userState.data.find { it.chatId == chatId } ?: ChatData(chatId, state = BotStates.WAIT_CATEGORY)
@@ -229,7 +262,6 @@ class BotService(
sortBy = "name", sortBy = "name",
direction = "ASC" direction = "ASC"
) )
.awaitSingle()
.first { it.id == update.callbackQuery.data.split("_")[1] } .first { it.id == update.callbackQuery.data.split("_")[1] }
val space = spaceService.getSpace("67af3c0f652da946a7dd9931") val space = spaceService.getSpace("67af3c0f652da946a7dd9931")
val instantType = financialService.getTransactionTypes().first { it.code == "INSTANT" } val instantType = financialService.getTransactionTypes().first { it.code == "INSTANT" }

View File

@@ -3,6 +3,7 @@ package space.luminic.budgerapp.services
import kotlinx.coroutines.reactive.awaitFirstOrNull import kotlinx.coroutines.reactive.awaitFirstOrNull
import kotlinx.coroutines.reactive.awaitSingle import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.coroutines.reactor.awaitSingle
import kotlinx.coroutines.reactor.awaitSingleOrNull import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.bson.Document import org.bson.Document
import org.bson.types.ObjectId import org.bson.types.ObjectId
@@ -28,7 +29,7 @@ class CategoryService(
private val mongoTemplate: ReactiveMongoTemplate, private val mongoTemplate: ReactiveMongoTemplate,
private val categoryMapper: CategoryMapper, private val categoryMapper: CategoryMapper,
private val budgetRepo: BudgetRepo, private val budgetRepo: BudgetRepo,
private val nlpService: NLPService
) { ) {
private val logger = LoggerFactory.getLogger(javaClass) private val logger = LoggerFactory.getLogger(javaClass)
@@ -67,14 +68,15 @@ class CategoryService(
}.awaitFirstOrNull() ?: throw NotFoundException("Category not found") }.awaitFirstOrNull() ?: throw NotFoundException("Category not found")
} }
@Cacheable(cacheNames = ["categories"])
fun getCategories( suspend fun getCategories(
spaceId: String, spaceId: String,
type: String? = null, type: String? = null,
sortBy: String, sortBy: String,
direction: String, direction: String,
tagCode: String? = null tagCode: String? = null,
): Mono<List<Category>> { predict: String? = null
): MutableList<Category> {
val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails") val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails")
val unwindSpace = unwind("spaceDetails") val unwindSpace = unwind("spaceDetails")
val matchCriteria = mutableListOf<Criteria>() val matchCriteria = mutableListOf<Criteria>()
@@ -99,7 +101,7 @@ class CategoryService(
).filterNotNull() ).filterNotNull()
val aggregation = newAggregation(aggregationBuilder) val aggregation = newAggregation(aggregationBuilder)
return mongoTemplate.aggregate( val categories = mongoTemplate.aggregate(
aggregation, "categories", Document::class.java aggregation, "categories", Document::class.java
) )
.collectList() // Преобразуем Flux<Transaction> в Mono<List<Transaction>> .collectList() // Преобразуем Flux<Transaction> в Mono<List<Transaction>>
@@ -107,8 +109,19 @@ class CategoryService(
docs.map { doc -> docs.map { doc ->
categoryMapper.fromDocument(doc) categoryMapper.fromDocument(doc)
} }
}.awaitSingle().toMutableList()
val predictedCategories = mutableListOf<CategoryPrediction>()
if (!predict.isNullOrBlank()) {
predictedCategories.addAll(nlpService.predictCategory(predict, 1))
} }
val filteredCategories = mutableListOf<Category>()
for (category in predictedCategories) {
categories.find { it.name == category.category }?.let { filteredCategories.add(it) }
} }
return if (filteredCategories.isEmpty()) categories else filteredCategories
}
@Cacheable("categoryTypes") @Cacheable("categoryTypes")
fun getCategoryTypes(): List<CategoryType> { fun getCategoryTypes(): List<CategoryType> {

View File

@@ -47,7 +47,7 @@ class FinancialService(
private val categoryRepo: CategoryRepo, private val categoryRepo: CategoryRepo,
val transactionsMapper: TransactionsMapper, val transactionsMapper: TransactionsMapper,
val budgetMapper: BudgetMapper, val budgetMapper: BudgetMapper,
private val subscriptionService: SubscriptionService private val subscriptionService: SubscriptionService,
) { ) {
private val logger = LoggerFactory.getLogger(FinancialService::class.java) private val logger = LoggerFactory.getLogger(FinancialService::class.java)
@@ -57,8 +57,14 @@ class FinancialService(
transaction.space!!.id!!, budgetId = null, transaction.date, transaction.date transaction.space!!.id!!, budgetId = null, transaction.date, transaction.date
) )
if (transaction.category.type.code == "EXPENSE") { if (transaction.category.type.code == "EXPENSE") {
val budgetCategory = budget.categories.firstOrNull { it.category.id == transaction.category.id } var budgetCategory = budget.categories.firstOrNull { it.category.id == transaction.category.id }
?: throw NotFoundException("Budget category not found in the budget")
if (budgetCategory == null) {
budgetCategory = BudgetCategory(0.0, 0.0, 0.0, transaction.category)
budget.categories.add(budgetCategory)
budgetRepo.save(budget).awaitSingle()
}
if (transaction.category.type.code == "INCOME") { if (transaction.category.type.code == "INCOME") {
budgetRepo.save(budget).awaitSingle() budgetRepo.save(budget).awaitSingle()
} }
@@ -845,6 +851,7 @@ class FinancialService(
} }
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
suspend fun createTransaction(space: Space, transaction: Transaction, user: User? = null): Transaction { suspend fun createTransaction(space: Space, transaction: Transaction, user: User? = null): Transaction {
@@ -930,12 +937,15 @@ class FinancialService(
"main" -> { "main" -> {
"Пользователь ${author.username} изменил $transactionType транзакцию:\n$sb" "Пользователь ${author.username} изменил $transactionType транзакцию:\n$sb"
} }
"done_true" -> { "done_true" -> {
"Пользователь ${author.username} выполнил ${transaction.comment} с суммой ${transaction.amount.toInt()}" "Пользователь ${author.username} выполнил ${transaction.comment} с суммой ${transaction.amount.toInt()}"
} }
"done_false" -> { "done_false" -> {
"Пользователь ${author.username} отменил выполнение ${transaction.comment} с суммой ${transaction.amount.toInt()}" "Пользователь ${author.username} отменил выполнение ${transaction.comment} с суммой ${transaction.amount.toInt()}"
} }
else -> "Изменения не обнаружены, но что то точно изменилось" else -> "Изменения не обнаружены, но что то точно изменилось"
} }

View File

@@ -0,0 +1,30 @@
package space.luminic.budgerapp.services
import kotlinx.coroutines.reactive.awaitSingle
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.client.WebClient
import space.luminic.budgerapp.configs.NLPConfig
import space.luminic.budgerapp.models.CategoryPrediction
@Service
class NLPService(
private val nlpConfig: NLPConfig,
private val webClient: WebClient = WebClient.builder()
.baseUrl(nlpConfig.address)
// .defaultHeader("Authorization", "Bearer YOUR_API_KEY")
.build(),
) {
suspend fun predictCategory(category: String, cloud: Int): List<CategoryPrediction> {
val response = webClient.get().uri("/predict?req=$category&cloud=$cloud")
.retrieve()
.bodyToFlux(CategoryPrediction::class.java)
.collectList()
.awaitSingle()
return response
}
}

View File

@@ -140,7 +140,7 @@ class SpaceService(
launch { launch {
val categories = val categories =
categoryService.getCategories(objectId.toString(), null, "name", "ASC").awaitFirstOrNull().orEmpty() categoryService.getCategories(objectId.toString(), null, "name", "ASC")
categoryRepo.deleteAll(categories).awaitFirstOrNull() categoryRepo.deleteAll(categories).awaitFirstOrNull()
} }
@@ -280,7 +280,6 @@ class SpaceService(
val existedTag = findTag(space, tagCode) ?: throw NoSuchElementException("Tag with code $tagCode not found") val existedTag = findTag(space, tagCode) ?: throw NoSuchElementException("Tag with code $tagCode not found")
val categoriesWithTag = val categoriesWithTag =
categoryService.getCategories(space.id!!, sortBy = "name", direction = "ASC", tagCode = existedTag.code) categoryService.getCategories(space.id!!, sortBy = "name", direction = "ASC", tagCode = existedTag.code)
.awaitSingleOrNull().orEmpty()
categoriesWithTag.map { cat -> categoriesWithTag.map { cat ->
cat.tags.removeIf { it.code == tagCode } // Изменяем список тегов cat.tags.removeIf { it.code == tagCode } // Изменяем список тегов
cat cat

View File

@@ -6,3 +6,4 @@ spring.data.mongodb.uri=mongodb://budger-app:BA1q2w3e4r!@luminic.space:27017/bud
management.endpoints.web.exposure.include=* management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always management.endpoint.health.show-details=always
telegram.bot.token=6972242509:AAGyXuL3T-BNE4XMoo_qvtaYxw_SuiS_dDs telegram.bot.token=6972242509:AAGyXuL3T-BNE4XMoo_qvtaYxw_SuiS_dDs
nlp.address=http://127.0.0.1:8000

View File

@@ -17,4 +17,4 @@ logging.level.org.springframework.data.mongodb.code = DEBUG
telegram.bot.token = 6662300972:AAFXjk_h0AUCy4bORC12UcdXbYnh2QSVKAY telegram.bot.token = 6662300972:AAFXjk_h0AUCy4bORC12UcdXbYnh2QSVKAY
nlp.address=https://nlp.luminic.space

View File

@@ -4,11 +4,13 @@ server.port=8082
#server.servlet.context-path=/api #server.servlet.context-path=/api
spring.webflux.base-path=/api spring.webflux.base-path=/api
spring.profiles.active=prod spring.profiles.active=dev
spring.main.web-application-type=reactive spring.main.web-application-type=reactive
logging.level.org.springframework.web=INFO logging.level.org.springframework.web=INFO
logging.level.org.springframework.data = INFO logging.level.org.springframework.data = INFO
logging.level.org.springframework.data.mongodb.core.ReactiveMongoTemplate=INFO logging.level.org.springframework.data.mongodb.core.ReactiveMongoTemplate=INFO