Compare commits

...

2 Commits

Author SHA1 Message Date
kashiuno
e84fa3ce3e Rest, getting slots for partner 2025-02-23 11:18:56 +03:00
kashiuno
2211280078 Fix schema to latest version 2025-02-19 17:08:51 +03:00
15 changed files with 250 additions and 53 deletions

View File

@ -1,48 +1,50 @@
plugins { plugins {
kotlin("jvm") version "1.9.25" kotlin("jvm") version "1.9.25"
kotlin("plugin.spring") version "1.9.25" kotlin("plugin.spring") version "1.9.25"
id("org.springframework.boot") version "3.4.1" id("org.springframework.boot") version "3.4.1"
id("io.spring.dependency-management") version "1.1.7" id("io.spring.dependency-management") version "1.1.7"
} }
group = "ru.vyatsu" group = "ru.vyatsu"
version = "1.0.0" version = "1.0.0"
java { java {
toolchain { toolchain {
languageVersion = JavaLanguageVersion.of(17) languageVersion = JavaLanguageVersion.of(17)
} }
} }
repositories { repositories {
mavenLocal() mavenLocal()
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jdbc") implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server") implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect") compileOnly("org.projectlombok:lombok")
implementation("org.liquibase:liquibase-core") annotationProcessor("org.projectlombok:lombok")
implementation("ru.vyatsu:qr-access-hardware-contract:1.0.0") implementation("org.jetbrains.kotlin:kotlin-reflect")
runtimeOnly("org.postgresql:postgresql") implementation("org.liquibase:liquibase-core")
testImplementation("org.springframework.boot:spring-boot-starter-test") implementation("ru.vyatsu:qr-access-hardware-contract:1.0.0")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") runtimeOnly("org.postgresql:postgresql")
testImplementation("org.springframework.security:spring-security-test") testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.testcontainers:postgresql") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher") testImplementation("org.springframework.security:spring-security-test")
implementation("org.yaml:snakeyaml") testImplementation("org.testcontainers:postgresql")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
implementation("org.yaml:snakeyaml")
} }
kotlin { kotlin {
compilerOptions { compilerOptions {
freeCompilerArgs.addAll("-Xjsr305=strict") freeCompilerArgs.addAll("-Xjsr305=strict")
} }
} }
tasks.withType<Test> { tasks.withType<Test> {
useJUnitPlatform() useJUnitPlatform()
} }

View File

@ -13,7 +13,10 @@ class SecurityConfig {
@Bean @Bean
fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain { fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
return http.authorizeHttpRequests { it.anyRequest().authenticated() } return http.authorizeHttpRequests {
it.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
}
.oauth2ResourceServer { it.jwt(Customizer.withDefaults()) } .oauth2ResourceServer { it.jwt(Customizer.withDefaults()) }
.build() .build()
} }

View File

@ -0,0 +1,18 @@
package ru.vyatsu.qr_access_api.slots.controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import ru.vyatsu.qr_access_api.slots.response.SlotResponse
import ru.vyatsu.qr_access_api.slots.service.SlotService
@RestController
@RequestMapping("public")
class PublicSlotsController(val service: SlotService) {
@GetMapping("/slots/{partnerId}")
fun getSlots(@PathVariable partnerId: String): SlotResponse {
return service.getAllSlotsByPartner(partnerId)
}
}

View File

@ -0,0 +1,81 @@
package ru.vyatsu.qr_access_api.slots.repository
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.stereotype.Repository
import ru.vyatsu.qr_access_api.slots.repository.entity.*
const val FIND_DOORS_SCHEDULE_BY_PARTNER_ID =
"select d.id, d.description, d.count, s.date, s.start_time, s.end_time from oauth2_registered_client u join doors d on (d.unit_id = u.client_id) join schedule s on (d.id = s.door_id) where u.partner_id = ?"
const val FIND_RENT_DOORS_BY_DOOR_IDS =
"select d.id, r.date, r.start_time, r.end_time from oauth2_registered_client u join doors d on (d.unit_id = u.client_id) join rent r on (r.door_id = d.id) where u.partner_id = ? order by r.start_time asc"
@Repository
class SlotRepository(private val jdbc: JdbcTemplate) {
fun findAllDoorsWithScheduleByPartnerId(partnerId: String): Collection<DoorWithSchedule> {
val doors: MutableMap<String, DoorWithSchedule> = mutableMapOf()
jdbc.query({
val stmt = it.prepareStatement(FIND_DOORS_SCHEDULE_BY_PARTNER_ID)
stmt.setString(1, partnerId)
stmt
}, { rs ->
val id = rs.getString("id")
doors[id]?.also {
it.schedule.add(
ScheduleEntry(
rs.getTime("start_time").toLocalTime(),
rs.getTime("end_time").toLocalTime(),
rs.getDate("date").toLocalDate()
)
)
} ?: run {
doors[id] = DoorWithSchedule(
id, rs.getString("description"), rs.getInt("count"), mutableListOf(
ScheduleEntry(
rs.getTime("start_time").toLocalTime(),
rs.getTime("end_time").toLocalTime(),
rs.getDate("date").toLocalDate()
)
)
)
}
})
return doors.values
}
fun findRentDateTimesByDoorsId(partnerId: String): DoorRent {
val doors = DoorRent(mutableMapOf())
jdbc.query({
val stmt = it.prepareStatement(FIND_RENT_DOORS_BY_DOOR_IDS)
stmt.setString(1, partnerId)
stmt
}, { rs ->
val id = rs.getString("id")
val date = rs.getDate("date").toLocalDate()
doors.dates[id]?.also { d ->
d.rentDate[date]?.also { times ->
times.add(RentTime(rs.getTime("start_time").toLocalTime(), rs.getTime("end_time").toLocalTime()))
} ?: run {
d.rentDate[date] = mutableListOf(
RentTime(
rs.getTime("start_time").toLocalTime(),
rs.getTime("end_time").toLocalTime()
)
)
}
} ?: run {
doors.dates[id] = RentDate(
mutableMapOf(
date to mutableListOf(
RentTime(
rs.getTime("start_time").toLocalTime(),
rs.getTime("end_time").toLocalTime()
)
)
)
)
}
})
return doors
}
}

View File

@ -0,0 +1,3 @@
package ru.vyatsu.qr_access_api.slots.repository.entity
data class DoorRent(val dates: MutableMap<String, RentDate>)

View File

@ -0,0 +1,8 @@
package ru.vyatsu.qr_access_api.slots.repository.entity
data class DoorWithSchedule(
val id: String,
val description: String,
val count: Int,
val schedule: MutableList<ScheduleEntry>
)

View File

@ -0,0 +1,5 @@
package ru.vyatsu.qr_access_api.slots.repository.entity
import java.time.LocalDate
data class RentDate(val rentDate: MutableMap<LocalDate, MutableList<RentTime>>)

View File

@ -0,0 +1,5 @@
package ru.vyatsu.qr_access_api.slots.repository.entity
import java.time.LocalTime
data class RentTime(val startTime: LocalTime, val endTime: LocalTime)

View File

@ -0,0 +1,6 @@
package ru.vyatsu.qr_access_api.slots.repository.entity
import java.time.LocalDate
import java.time.LocalTime
data class ScheduleEntry(val startTime: LocalTime, val endTime: LocalTime, val date: LocalDate)

View File

@ -0,0 +1,3 @@
package ru.vyatsu.qr_access_api.slots.response
data class Door(val id: String, val description: String, val slots: List<Slot>)

View File

@ -0,0 +1,6 @@
package ru.vyatsu.qr_access_api.slots.response
import java.time.LocalDate
import java.time.LocalTime
data class Slot(val startTime: LocalTime, val endTime: LocalTime, val date: LocalDate, val status: SlotStatus)

View File

@ -0,0 +1,3 @@
package ru.vyatsu.qr_access_api.slots.response
data class SlotResponse(val doors: List<Door>)

View File

@ -0,0 +1,5 @@
package ru.vyatsu.qr_access_api.slots.response
enum class SlotStatus {
FREE, BOOKED, OUT_OF_WORKING_TIME
}

View File

@ -0,0 +1,51 @@
package ru.vyatsu.qr_access_api.slots.service
import org.springframework.stereotype.Service
import ru.vyatsu.qr_access_api.slots.repository.SlotRepository
import ru.vyatsu.qr_access_api.slots.response.Door
import ru.vyatsu.qr_access_api.slots.response.Slot
import ru.vyatsu.qr_access_api.slots.response.SlotResponse
import ru.vyatsu.qr_access_api.slots.response.SlotStatus
import java.time.LocalDate
import java.time.LocalTime
@Service
class SlotService(private val repository: SlotRepository) {
fun getAllSlotsByPartner(partnerId: String): SlotResponse {
// TODO: Учитывать количество мест и переписать логику
val doorsWithSchedule = repository.findAllDoorsWithScheduleByPartnerId(partnerId)
val doorRents = repository.findRentDateTimesByDoorsId(partnerId)
val doors: MutableList<Door> = mutableListOf()
doorsWithSchedule.forEach { d ->
val slots: MutableList<Slot> = mutableListOf()
d.schedule.forEach { sch ->
val rentTimes = doorRents.dates[d.id]?.rentDate?.get(sch.date) ?: listOf()
createIntermediateSlot(LocalTime.MIN, sch.startTime, sch.date, SlotStatus.OUT_OF_WORKING_TIME)
?.also { slots.add(it) }
rentTimes.forEach { rt ->
val lastSlot: Slot? = if (slots.lastIndex == -1) null else slots.last()
createIntermediateSlot(lastSlot?.endTime, rt.startTime, sch.date, SlotStatus.FREE)
?.also { slots.add(it) }
slots.add(Slot(rt.startTime, rt.endTime, sch.date, SlotStatus.BOOKED))
}
var lastSlot: Slot? = if (slots.lastIndex == -1) null else slots.last()
createIntermediateSlot(lastSlot?.endTime, sch.endTime, sch.date, SlotStatus.FREE)
?.also { slots.add(it) }
lastSlot = if (slots.lastIndex == -1) null else slots.last()
createIntermediateSlot(lastSlot?.endTime, LocalTime.MAX, sch.date, SlotStatus.OUT_OF_WORKING_TIME)
?.also { slots.add(it) }
}
doors.add(Door(d.id, d.description, slots))
}
return SlotResponse(doors)
}
private fun createIntermediateSlot(
startTime: LocalTime?,
endTime: LocalTime,
date: LocalDate,
status: SlotStatus
): Slot? {
return if (startTime != null && startTime != endTime) Slot(startTime, endTime, date, status) else null
}
}

View File

@ -128,12 +128,30 @@ databaseChangeLog:
constraints: constraints:
nullable: false nullable: false
- column: - column:
name: parent_door_id name: parent_door_ids
type: TEXT type: TEXT
constraints: constraints:
nullable: true nullable: true
foreignKeyName: FK_parent_doors - createTable:
references: doors(id) tableName: clients
columns:
- column:
constraints:
nullable: false
primaryKey: true
primaryKeyName: PK_clients
name: id
type: TEXT
- column:
constraints:
nullable: false
name: email
type: TEXT
- column:
constraints:
nullable: false
name: email_is_confirmed
type: BOOLEAN
- createTable: - createTable:
tableName: rent tableName: rent
columns: columns:
@ -160,7 +178,7 @@ databaseChangeLog:
constraints: constraints:
nullable: false nullable: false
foreignKeyName: FK_rent_clients foreignKeyName: FK_rent_clients
references: rent(id) references: clients(id)
- column: - column:
name: date name: date
type: DATE type: DATE
@ -188,26 +206,6 @@ databaseChangeLog:
type: TIMESTAMP WITH TIME ZONE type: TIMESTAMP WITH TIME ZONE
constraints: constraints:
nullable: false nullable: false
- createTable:
tableName: clients
columns:
- column:
constraints:
nullable: false
primaryKey: true
primaryKeyName: PK_clients
name: id
type: TEXT
- column:
constraints:
nullable: false
name: email
type: TEXT
- column:
constraints:
nullable: false
name: email_is_confirmed
type: BOOLEAN
- createTable: - createTable:
tableName: schedule tableName: schedule
columns: columns: