backedn #6

Open
student-32598 wants to merge 2 commits from student-32598/NTO-2025-Client-Android-backend:backedn into main
15 changed files with 294 additions and 29 deletions

View File

@@ -12,13 +12,24 @@ application {
}
dependencies {
// СЕРВЕРНЫЕ ЗАВИСИМОСТИ KTOR
implementation(libs.ktor.server.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.netty)
implementation(libs.logback.classic)
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.config.yaml)
// HTTP-клиент для Kotlin/JVM (не Android!)
implementation("io.ktor:ktor-client-cio:2.3.9")
implementation("io.ktor:ktor-client-content-negotiation:2.3.9")
implementation("io.ktor:ktor-client-logging:2.3.9")
// Корутины и сериализация
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
// Тесты
testImplementation(libs.ktor.server.test.host)
testImplementation(libs.kotlin.test.junit)
}

View File

@@ -2,6 +2,7 @@
kotlin = "2.2.20"
ktor = "3.3.0"
logback = "1.4.14"
lifecycleViewmodelAndroid = "2.10.0"
[libraries]
ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" }
@@ -12,6 +13,7 @@ logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "lo
ktor-server-config-yaml = { module = "io.ktor:ktor-server-config-yaml", version.ref = "ktor" }
ktor-server-test-host = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" }
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
lifecycle-viewmodel-android = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-android", version.ref = "lifecycleViewmodelAndroid" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }

8
local.properties Normal file
View File

@@ -0,0 +1,8 @@
## This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
#Fri Dec 12 08:17:55 YEKT 2025
sdk.dir=D\:\\androidstudio

0
src/main/kotlin/Application.kt Executable file → Normal file
View File

37
src/main/kotlin/Main.kt Normal file
View File

@@ -0,0 +1,37 @@
package com.example
import com.example.di.Dependencies
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.delay
fun main() = runBlocking {
val viewModel = Dependencies.createBookingViewModel()
// Настраиваем колбэки
viewModel.onBookingsUpdated = { bookings ->
println("Бронирования обновлены:")
bookings.forEach { booking ->
println(" Комната: ${booking.room}, Время: ${booking.time}")
}
}
viewModel.onError = { error ->
if (error != null) {
println("Ошибка: $error")
}
}
// Загружаем текущие бронирования
println("Загружаем бронирования...")
viewModel.loadBookings()
delay(1000) // Ждём загрузки
// Добавляем новое бронирование
println("\nДобавляем новое бронирование...")
viewModel.addBooking("505.6", "14:00 - 16:00")
delay(1000) // Ждём обработки
viewModel.dispose()
}

View File

@@ -8,6 +8,7 @@ import io.ktor.http.content.PartData
import io.ktor.http.content.forEachPart
import io.ktor.server.application.*
import io.ktor.server.request.receiveMultipart
import io.ktor.server.request.receiveParameters
import io.ktor.server.response.*
import io.ktor.server.routing.*
@@ -27,38 +28,29 @@ fun Application.configureRouting() {
get("/user") {
call.respond(UserDto(booking = booking))
}
// ЗАМЕНИТЕ ЭТОТ БЛОК (от post("/book") до закрывающей скобки):
post("/book") {
var room = ""
var time = ""
val multipartData = call.receiveMultipart()
multipartData.forEachPart { part ->
try {
if (part !is PartData.FormItem) return@forEachPart
when (part.name) {
"room" -> {
room = part.value.trim()
}
// Получаем данные в формате x-www-form-urlencoded
val parameters = call.receiveParameters()
val room = parameters["room"]?.trim() ?: ""
val time = parameters["time"]?.trim() ?: ""
"time" -> {
time = part.value.trim()
}
else -> Unit
}
} finally {
part.dispose()
}
}
if (room.isEmpty() && time.isEmpty()) {
if (room.isEmpty() || time.isEmpty()) {
call.respond(
HttpStatusCode.BadRequest,
ErrorDto(error = "Field is empty")
ErrorDto(error = "Fields 'room' and 'time' are required")
)
} else {
booking.add(BookingDto(room = room, time = time))
call.respond(HttpStatusCode.OK)
call.respond(HttpStatusCode.OK, mapOf("success" to true))
}
} catch (e: Exception) {
call.respond(
HttpStatusCode.BadRequest,
ErrorDto(error = "Invalid request format")
)
}
}
}

View File

@@ -0,0 +1,11 @@
package com.example.data.mapper
import com.example.data.remote.dto.BookingResponse
import com.example.domain.models.Booking
fun BookingResponse.toDomain(): Booking {
return Booking(
room = room,
time = time
)
}

View File

@@ -0,0 +1,37 @@
package com.example.data.remote
import com.example.data.remote.dto.BookingResponse
import com.example.data.remote.dto.UserResponse
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.client.request.forms.*
interface BookingApi {
suspend fun getUser(): UserResponse
suspend fun bookRoom(room: String, time: String): Boolean
}
class BookingApiImpl(private val client: HttpClient) : BookingApi {
private val baseUrl = "http://localhost:8080"
override suspend fun getUser(): UserResponse {
return client.get("$baseUrl/user").body()
}
override suspend fun bookRoom(room: String, time: String): Boolean {
return try {
val response = client.submitForm(
url = "$baseUrl/book",
formParameters = Parameters.build {
append("room", room)
append("time", time)
}
)
response.status == HttpStatusCode.OK
} catch (e: Exception) {
false
}
}
}

View File

@@ -0,0 +1,20 @@
package com.example.data.remote.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class UserResponse(
@SerialName("name")
val name: String,
@SerialName("booking")
val booking: List<BookingResponse>
)
@Serializable
data class BookingResponse(
@SerialName("room")
val room: String,
@SerialName("time")
val time: String
)

View File

@@ -0,0 +1,23 @@
package com.example.data.repository
import com.example.data.mapper.toDomain
import com.example.data.remote.BookingApi
import com.example.domain.models.Booking
import com.example.domain.repository.BookingRepository
class BookingRepositoryImpl(
private val api: BookingApi
) : BookingRepository {
override suspend fun getBookings(): List<Booking> {
return try {
api.getUser().booking.map { it.toDomain() }
} catch (e: Exception) {
emptyList()
}
}
override suspend fun addBooking(room: String, time: String): Boolean {
return api.bookRoom(room, time)
}
}

View File

@@ -0,0 +1,41 @@
package com.example.di
import com.example.data.remote.BookingApi
import com.example.data.remote.BookingApiImpl
import com.example.data.repository.BookingRepositoryImpl
import com.example.domain.repository.BookingRepository
import com.example.presentation.BookingViewModel
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
object Dependencies {
val httpClient: HttpClient by lazy {
HttpClient(CIO) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
isLenient = true
})
}
install(Logging) {
level = LogLevel.ALL
}
}
}
val bookingApi: BookingApi by lazy {
BookingApiImpl(httpClient)
}
val bookingRepository: BookingRepository by lazy {
BookingRepositoryImpl(bookingApi)
}
fun createBookingViewModel(): BookingViewModel {
return BookingViewModel(bookingRepository)
}
}

View File

@@ -0,0 +1,6 @@
package com.example.domain.models
data class Booking(
val room: String,
val time: String
)

View File

@@ -0,0 +1,6 @@
package com.example.domain.models
data class User(
val name: String,
val bookings: List<Booking>
)

View File

@@ -0,0 +1,8 @@
package com.example.domain.repository
import com.example.domain.models.Booking
interface BookingRepository {
suspend fun getBookings(): List<Booking>
suspend fun addBooking(room: String, time: String): Boolean
}

View File

@@ -0,0 +1,63 @@
package com.example.presentation
import com.example.domain.models.Booking
import com.example.domain.repository.BookingRepository
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
class BookingViewModel(
private val repository: BookingRepository,
private val coroutineContext: CoroutineContext = Dispatchers.Default
) {
private var bookings: List<Booking> = emptyList()
private var error: String? = null
private val scope = CoroutineScope(coroutineContext)
// Колбэки для обновления UI
var onBookingsUpdated: (List<Booking>) -> Unit = {}
var onError: (String?) -> Unit = {}
fun loadBookings() {
scope.launch {
try {
bookings = repository.getBookings()
error = null
onBookingsUpdated(bookings)
onError(null)
} catch (e: Exception) {
error = "Ошибка загрузки: ${e.message}"
onError(error)
}
}
}
fun addBooking(room: String, time: String) {
if (room.isBlank() || time.isBlank()) {
error = "Заполните все поля"
onError(error)
return
}
scope.launch {
try {
val success = repository.addBooking(room, time)
if (success) {
loadBookings() // Обновляем список
} else {
error = "Ошибка бронирования"
onError(error)
}
} catch (e: Exception) {
error = "Ошибка: ${e.message}"
onError(error)
}
}
}
fun getCurrentBookings(): List<Booking> = bookings
fun getCurrentError(): String? = error
fun dispose() {
scope.cancel()
}
}