+ nlp
This commit is contained in:
@@ -2,12 +2,14 @@ package space.luminic.budgerapp
|
||||
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.cache.annotation.EnableCaching
|
||||
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories
|
||||
import org.springframework.scheduling.annotation.EnableAsync
|
||||
import org.springframework.scheduling.annotation.EnableScheduling
|
||||
import space.luminic.budgerapp.configs.NLPConfig
|
||||
import space.luminic.budgerapp.configs.TelegramBotProperties
|
||||
import java.util.*
|
||||
|
||||
@@ -15,7 +17,8 @@ import java.util.*
|
||||
@EnableCaching
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
@EnableConfigurationProperties(TelegramBotProperties::class)
|
||||
//@EnableConfigurationProperties([TelegramBotProperties::class,)
|
||||
@ConfigurationPropertiesScan(basePackages = ["space.luminic.budgerapp"])
|
||||
@EnableMongoRepositories(basePackages = ["space.luminic.budgerapp.repos"])
|
||||
class BudgerAppApplication
|
||||
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
package space.luminic.budgerapp.configs
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
//@Configuration
|
||||
|
||||
//class CommonConfig {
|
||||
// @Bean
|
||||
// fun httpTraceRepository(): HttpTraceRepository {
|
||||
// return InMemoryHttpTraceRepository()
|
||||
// }
|
||||
//}
|
||||
//}
|
||||
|
||||
|
||||
@ConfigurationProperties(prefix = "nlp")
|
||||
data class NLPConfig(
|
||||
val address: String,
|
||||
)
|
||||
@@ -1,18 +1,25 @@
|
||||
package space.luminic.budgerapp.controllers
|
||||
|
||||
import com.opencsv.CSVWriter
|
||||
import kotlinx.coroutines.reactor.awaitSingle
|
||||
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
||||
import org.apache.commons.io.IOUtils.writer
|
||||
import org.bson.Document
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import space.luminic.budgerapp.controllers.BudgetController.LimitValue
|
||||
import space.luminic.budgerapp.controllers.dtos.BudgetCreationDTO
|
||||
import space.luminic.budgerapp.models.*
|
||||
import space.luminic.budgerapp.services.*
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.OutputStreamWriter
|
||||
import java.time.LocalDate
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/spaces")
|
||||
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}")
|
||||
suspend fun getTransaction(
|
||||
@@ -181,6 +245,7 @@ class SpaceController(
|
||||
return financialService.getTransactionById(id)
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/{spaceId}/transactions")
|
||||
suspend fun createTransaction(@PathVariable spaceId: String, @RequestBody transaction: Transaction): Transaction {
|
||||
val user = authService.getSecurityUser()
|
||||
@@ -220,7 +285,7 @@ class SpaceController(
|
||||
): List<Category> {
|
||||
val user = authService.getSecurityUser()
|
||||
spaceService.isValidRequest(spaceId, user)
|
||||
return categoryService.getCategories(spaceId, type, sortBy, direction).awaitSingleOrNull().orEmpty()
|
||||
return categoryService.getCategories(spaceId, type, sortBy, direction)
|
||||
}
|
||||
|
||||
@GetMapping("/{spaceId}/categories/types")
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package space.luminic.budgerapp.models
|
||||
|
||||
data class CategoryPrediction(val category: String, val weight: Double) {
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import org.springframework.stereotype.Service
|
||||
import org.telegram.telegrambots.bots.TelegramLongPollingBot
|
||||
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.EditMessageReplyMarkup
|
||||
import org.telegram.telegrambots.meta.api.methods.updatingmessages.EditMessageText
|
||||
import org.telegram.telegrambots.meta.api.objects.Update
|
||||
import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup
|
||||
@@ -35,9 +36,76 @@ class BotService(
|
||||
private val categoriesService: CategoryService,
|
||||
private val financialService: FinancialService,
|
||||
private val spaceService: SpaceService,
|
||||
private val nlpService: NLPService
|
||||
) : TelegramLongPollingBot(telegramBotProperties.token) {
|
||||
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 {
|
||||
return telegramBotProperties.username
|
||||
}
|
||||
@@ -85,6 +153,12 @@ class BotService(
|
||||
val deleteMsg = DeleteMessage(chatId, update.callbackQuery.message.messageId)
|
||||
execute(deleteMsg)
|
||||
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 "
|
||||
}
|
||||
|
||||
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()
|
||||
message.chatId = chatId
|
||||
val msg = "Выберите категорию"
|
||||
message.text = msg
|
||||
message.replyMarkup = keyboard
|
||||
message.replyMarkup = constructCategoriesButtons(true, text, chatId.toLong())
|
||||
val userState = BotUserState(user = user)
|
||||
val chatData =
|
||||
userState.data.find { it.chatId == chatId } ?: ChatData(chatId, state = BotStates.WAIT_CATEGORY)
|
||||
@@ -229,7 +262,6 @@ class BotService(
|
||||
sortBy = "name",
|
||||
direction = "ASC"
|
||||
)
|
||||
.awaitSingle()
|
||||
.first { it.id == update.callbackQuery.data.split("_")[1] }
|
||||
val space = spaceService.getSpace("67af3c0f652da946a7dd9931")
|
||||
val instantType = financialService.getTransactionTypes().first { it.code == "INSTANT" }
|
||||
|
||||
@@ -3,6 +3,7 @@ package space.luminic.budgerapp.services
|
||||
|
||||
import kotlinx.coroutines.reactive.awaitFirstOrNull
|
||||
import kotlinx.coroutines.reactive.awaitSingle
|
||||
import kotlinx.coroutines.reactor.awaitSingle
|
||||
import kotlinx.coroutines.reactor.awaitSingleOrNull
|
||||
import org.bson.Document
|
||||
import org.bson.types.ObjectId
|
||||
@@ -28,8 +29,8 @@ class CategoryService(
|
||||
private val mongoTemplate: ReactiveMongoTemplate,
|
||||
private val categoryMapper: CategoryMapper,
|
||||
private val budgetRepo: BudgetRepo,
|
||||
|
||||
) {
|
||||
private val nlpService: NLPService
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
@@ -67,14 +68,15 @@ class CategoryService(
|
||||
}.awaitFirstOrNull() ?: throw NotFoundException("Category not found")
|
||||
}
|
||||
|
||||
|
||||
fun getCategories(
|
||||
@Cacheable(cacheNames = ["categories"])
|
||||
suspend fun getCategories(
|
||||
spaceId: String,
|
||||
type: String? = null,
|
||||
sortBy: String,
|
||||
direction: String,
|
||||
tagCode: String? = null
|
||||
): Mono<List<Category>> {
|
||||
tagCode: String? = null,
|
||||
predict: String? = null
|
||||
): MutableList<Category> {
|
||||
val lookupSpaces = lookup("spaces", "space.\$id", "_id", "spaceDetails")
|
||||
val unwindSpace = unwind("spaceDetails")
|
||||
val matchCriteria = mutableListOf<Criteria>()
|
||||
@@ -99,7 +101,7 @@ class CategoryService(
|
||||
).filterNotNull()
|
||||
|
||||
val aggregation = newAggregation(aggregationBuilder)
|
||||
return mongoTemplate.aggregate(
|
||||
val categories = mongoTemplate.aggregate(
|
||||
aggregation, "categories", Document::class.java
|
||||
)
|
||||
.collectList() // Преобразуем Flux<Transaction> в Mono<List<Transaction>>
|
||||
@@ -107,9 +109,20 @@ class CategoryService(
|
||||
docs.map { 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")
|
||||
fun getCategoryTypes(): List<CategoryType> {
|
||||
val types = mutableListOf<CategoryType>()
|
||||
|
||||
@@ -47,7 +47,7 @@ class FinancialService(
|
||||
private val categoryRepo: CategoryRepo,
|
||||
val transactionsMapper: TransactionsMapper,
|
||||
val budgetMapper: BudgetMapper,
|
||||
private val subscriptionService: SubscriptionService
|
||||
private val subscriptionService: SubscriptionService,
|
||||
) {
|
||||
private val logger = LoggerFactory.getLogger(FinancialService::class.java)
|
||||
|
||||
@@ -57,8 +57,14 @@ class FinancialService(
|
||||
transaction.space!!.id!!, budgetId = null, transaction.date, transaction.date
|
||||
)
|
||||
if (transaction.category.type.code == "EXPENSE") {
|
||||
val budgetCategory = budget.categories.firstOrNull { it.category.id == transaction.category.id }
|
||||
?: throw NotFoundException("Budget category not found in the budget")
|
||||
var budgetCategory = budget.categories.firstOrNull { it.category.id == transaction.category.id }
|
||||
|
||||
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") {
|
||||
budgetRepo.save(budget).awaitSingle()
|
||||
}
|
||||
@@ -845,6 +851,7 @@ class FinancialService(
|
||||
|
||||
}
|
||||
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
suspend fun createTransaction(space: Space, transaction: Transaction, user: User? = null): Transaction {
|
||||
@@ -930,12 +937,15 @@ class FinancialService(
|
||||
"main" -> {
|
||||
"Пользователь ${author.username} изменил $transactionType транзакцию:\n$sb"
|
||||
}
|
||||
|
||||
"done_true" -> {
|
||||
"Пользователь ${author.username} выполнил ${transaction.comment} с суммой ${transaction.amount.toInt()}"
|
||||
}
|
||||
|
||||
"done_false" -> {
|
||||
"Пользователь ${author.username} отменил выполнение ${transaction.comment} с суммой ${transaction.amount.toInt()}"
|
||||
}
|
||||
|
||||
else -> "Изменения не обнаружены, но что то точно изменилось"
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -140,7 +140,7 @@ class SpaceService(
|
||||
|
||||
launch {
|
||||
val categories =
|
||||
categoryService.getCategories(objectId.toString(), null, "name", "ASC").awaitFirstOrNull().orEmpty()
|
||||
categoryService.getCategories(objectId.toString(), null, "name", "ASC")
|
||||
categoryRepo.deleteAll(categories).awaitFirstOrNull()
|
||||
}
|
||||
|
||||
@@ -280,7 +280,6 @@ class SpaceService(
|
||||
val existedTag = findTag(space, tagCode) ?: throw NoSuchElementException("Tag with code $tagCode not found")
|
||||
val categoriesWithTag =
|
||||
categoryService.getCategories(space.id!!, sortBy = "name", direction = "ASC", tagCode = existedTag.code)
|
||||
.awaitSingleOrNull().orEmpty()
|
||||
categoriesWithTag.map { cat ->
|
||||
cat.tags.removeIf { it.code == tagCode } // Изменяем список тегов
|
||||
cat
|
||||
|
||||
@@ -6,3 +6,4 @@ spring.data.mongodb.uri=mongodb://budger-app:BA1q2w3e4r!@luminic.space:27017/bud
|
||||
management.endpoints.web.exposure.include=*
|
||||
management.endpoint.health.show-details=always
|
||||
telegram.bot.token=6972242509:AAGyXuL3T-BNE4XMoo_qvtaYxw_SuiS_dDs
|
||||
nlp.address=http://127.0.0.1:8000
|
||||
@@ -17,4 +17,4 @@ logging.level.org.springframework.data.mongodb.code = DEBUG
|
||||
|
||||
|
||||
telegram.bot.token = 6662300972:AAFXjk_h0AUCy4bORC12UcdXbYnh2QSVKAY
|
||||
|
||||
nlp.address=https://nlp.luminic.space
|
||||
|
||||
@@ -4,11 +4,13 @@ server.port=8082
|
||||
#server.servlet.context-path=/api
|
||||
spring.webflux.base-path=/api
|
||||
|
||||
spring.profiles.active=prod
|
||||
spring.profiles.active=dev
|
||||
spring.main.web-application-type=reactive
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
logging.level.org.springframework.web=INFO
|
||||
logging.level.org.springframework.data = INFO
|
||||
logging.level.org.springframework.data.mongodb.core.ReactiveMongoTemplate=INFO
|
||||
|
||||
Reference in New Issue
Block a user