Compare commits
19
Commits
1a0319e0b4
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
309f46b329 | ||
|
|
105508c67f | ||
|
|
4007e9d86d | ||
|
|
911f971958 | ||
|
|
46bf29a5eb | ||
|
|
e84fa3ce3e | ||
|
|
2211280078 | ||
|
|
6abafedfec | ||
|
|
d7ac22e54e | ||
|
|
0e674b7a76 | ||
|
|
b0323aebff | ||
|
|
6356b162b8 | ||
|
|
195df55fcf | ||
|
|
a5c570ec42 | ||
|
|
9d658cea12 | ||
|
|
e3566ec66b | ||
|
|
1549abfac4 | ||
|
|
e5d833846e | ||
|
|
c6fddca8ea |
@@ -13,6 +13,7 @@ build/
|
|||||||
.settings
|
.settings
|
||||||
.springBeans
|
.springBeans
|
||||||
.sts4-cache
|
.sts4-cache
|
||||||
|
.gigaide
|
||||||
bin/
|
bin/
|
||||||
!**/src/main/**/bin/
|
!**/src/main/**/bin/
|
||||||
!**/src/test/**/bin/
|
!**/src/test/**/bin/
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Приложение предоставления API и логики формирования и отправки QR-кодов
|
||||||
|
|
||||||
|
Приложение предоставляет API для получения QR-кодов для дверей по идентификатору устройства, получение слотов для дверей по идентификатору партнера.
|
||||||
|
Реализует логику формирования и отправки QR-кодов, интеграцию с платежным шлюзом методом обратного вызова и методом опроса REST метода шлюза.
|
||||||
|
|
||||||
|
## Запуск приложения
|
||||||
|
|
||||||
|
Для приложения требуется БД и конфигурация подключения к БД.
|
||||||
|
При запуске приложения будут выполнены миграции в src/main/resources/db/changelog
|
||||||
|
Также для работы приложения нужен запущенный сервер авторизации
|
||||||
|
|
||||||
|
Для запуска приложения нужно выполнить команду
|
||||||
|
|
||||||
|
`gradle bootRun`
|
||||||
|
|
||||||
|
## Сборка приложения
|
||||||
|
|
||||||
|
`gradle jar`
|
||||||
|
|
||||||
|
## Задания по расписанию
|
||||||
|
|
||||||
|
- EmailOutboxSendJob - задание формирования и отправки QR-кода
|
||||||
|
- CleanExpiredNotPayedRentJob - задание по расписанию для опроса платежного шлюза и удаления просроченных не оплаченных бронирований
|
||||||
|
|
||||||
|
## Http методы
|
||||||
|
|
||||||
|
- POST /public/book - метод бронирования двери
|
||||||
|
- POST /public/book/callback - метод для обратного вызова шлюза
|
||||||
|
- GET /qr - метод получения QR-кодов для определенного устройства(информация об устройстве берется из токена)
|
||||||
|
- POST /qr-used/{qrId} - метод пометки о том, что qr-код был использован(для сбора метрик)
|
||||||
|
- GET /public/slots/{partnerId} - метод получения текущего расписания дверей для партнера
|
||||||
+32
-27
@@ -1,47 +1,52 @@
|
|||||||
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("org.springframework.boot:spring-boot-starter-mail")
|
||||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||||
implementation("org.liquibase:liquibase-core")
|
implementation("io.nayuki:qrcodegen:1.8.0")
|
||||||
implementation("ru.vyatsu:qr-access-hardware-contract:1.0.0")
|
compileOnly("org.projectlombok:lombok")
|
||||||
runtimeOnly("org.postgresql:postgresql")
|
annotationProcessor("org.projectlombok:lombok")
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
|
implementation("org.liquibase:liquibase-core")
|
||||||
testImplementation("org.springframework.security:spring-security-test")
|
implementation("ru.vyatsu:qr-access-hardware-contract:1.0.0")
|
||||||
testImplementation("org.testcontainers:postgresql")
|
runtimeOnly("org.postgresql:postgresql")
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
|
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
|
||||||
|
testImplementation("org.springframework.security:spring-security-test")
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,12 @@ package ru.vyatsu.qr_access_api
|
|||||||
|
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||||
import org.springframework.boot.runApplication
|
import org.springframework.boot.runApplication
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling
|
||||||
|
|
||||||
|
//TODO: Сделать ретраи везде, где в блок-схемах они указаны
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
|
@EnableScheduling
|
||||||
class QrAccessApiApplication
|
class QrAccessApiApplication
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package ru.vyatsu.qr_access_api.booking.controller
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import ru.vyatsu.qr_access_api.booking.request.BookCallbackRequest
|
||||||
|
import ru.vyatsu.qr_access_api.booking.service.BookingService
|
||||||
|
import ru.vyatsu.qr_access_api.booking.request.BookRequest
|
||||||
|
import ru.vyatsu.qr_access_api.booking.request.BookResponse
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
class BookingController(private val service: BookingService) {
|
||||||
|
// TODO: Убрать /public, так как эти методы должны быть закрыты авторизацией client_credential и корсами
|
||||||
|
@PostMapping("/public/book")
|
||||||
|
fun book(@RequestBody request: BookRequest): BookResponse {
|
||||||
|
return BookResponse(service.book(request))
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/public/book/callback")
|
||||||
|
fun bookCallback(@RequestBody request: BookCallbackRequest) {
|
||||||
|
return service.bookPayed(request.rentId)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package ru.vyatsu.qr_access_api.booking.job
|
||||||
|
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import ru.vyatsu.qr_access_api.booking.repository.BookingRepository
|
||||||
|
import ru.vyatsu.qr_access_api.common.logger
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class CleanExpiredNotPayedRentJob(private val repository: BookingRepository) {
|
||||||
|
|
||||||
|
@Scheduled(cron = "0 */5 * * * *")
|
||||||
|
fun clean() {
|
||||||
|
val deletedCount =
|
||||||
|
repository.deleteRentByDateCreatedLessThen(LocalDateTime.now().minusMinutes(10))
|
||||||
|
logger().info("CleanExpiredNotPayedRentJob deleted {} rents", deletedCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package ru.vyatsu.qr_access_api.booking.repository
|
||||||
|
|
||||||
|
import org.springframework.dao.EmptyResultDataAccessException
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate
|
||||||
|
import org.springframework.jdbc.support.GeneratedKeyHolder
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import ru.vyatsu.apis.NotFoundException
|
||||||
|
import ru.vyatsu.qr_access_api.booking.repository.entity.RentWithEmail
|
||||||
|
import java.sql.Date
|
||||||
|
import java.sql.Statement
|
||||||
|
import java.sql.Time
|
||||||
|
import java.sql.Timestamp
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.LocalTime
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
private const val ADD_BOOKING_INFO_QUERY =
|
||||||
|
"insert into rents(id, start_time, end_time, client_id, date, door_id, qr_code, payed, date_created) values (?, ? ,?, ?, ?, ?, ?, false, CURRENT_TIMESTAMP) RETURNING id"
|
||||||
|
|
||||||
|
private const val CREATE_NEW_CLIENT =
|
||||||
|
"insert into clients(id, email, email_is_confirmed) values (?, ?, false) RETURNING id"
|
||||||
|
|
||||||
|
private const val FIND_CLIENT_BY_EMAIL = "select id from clients where email = ?"
|
||||||
|
|
||||||
|
private const val MARK_RENT_AS_PAYED_QUERY = "update rents set payed=true where id = ?"
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
class BookingRepository(val template: JdbcTemplate) {
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun book(doorId: String, date: LocalDate, startTime: LocalTime, endTime: LocalTime, clientEmail: String): String {
|
||||||
|
val clientIdHolder = GeneratedKeyHolder()
|
||||||
|
val rentIdHolder = GeneratedKeyHolder()
|
||||||
|
var clientId: String? = null
|
||||||
|
try {
|
||||||
|
clientId = template.queryForObject(
|
||||||
|
FIND_CLIENT_BY_EMAIL,
|
||||||
|
{ rs, _ -> rs.getString("id") },
|
||||||
|
clientEmail
|
||||||
|
)
|
||||||
|
} catch (_: EmptyResultDataAccessException) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientId == null) {
|
||||||
|
template.update({
|
||||||
|
val stmt = it.prepareStatement(CREATE_NEW_CLIENT, Statement.RETURN_GENERATED_KEYS)
|
||||||
|
stmt.setString(1, UUID.randomUUID().toString())
|
||||||
|
stmt.setString(2, clientEmail)
|
||||||
|
stmt
|
||||||
|
}, clientIdHolder)
|
||||||
|
clientId = clientIdHolder.getKeyAs(String::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientId == null) throw RuntimeException("clientId is null even after insert, booking cannot be continued")
|
||||||
|
|
||||||
|
val insertedRows = template.update({
|
||||||
|
val stmt = it.prepareStatement(ADD_BOOKING_INFO_QUERY, Statement.RETURN_GENERATED_KEYS)
|
||||||
|
stmt.setString(1, UUID.randomUUID().toString())
|
||||||
|
stmt.setTime(2, Time.valueOf(startTime))
|
||||||
|
stmt.setTime(3, Time.valueOf(endTime))
|
||||||
|
stmt.setString(4, clientId)
|
||||||
|
stmt.setDate(5, Date.valueOf(date))
|
||||||
|
stmt.setString(6, doorId)
|
||||||
|
stmt.setString(7, UUID.randomUUID().toString())
|
||||||
|
stmt
|
||||||
|
}, rentIdHolder)
|
||||||
|
|
||||||
|
val insertedRentId = rentIdHolder.getKeyAs(String::class.java)
|
||||||
|
if (insertedRows <= 0 || insertedRentId == null) throw RuntimeException(
|
||||||
|
"Inserted rows number is invalid: %d. Have to be more then 0".format(
|
||||||
|
insertedRows
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return insertedRentId
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findRentWithClientById(id: String): RentWithEmail {
|
||||||
|
return template.queryForObject(
|
||||||
|
"select r.qr_code, c.email from rents r join clients c on (c.id = r.client_id) where r.id = ?",
|
||||||
|
{ rs, _ -> RentWithEmail(rs.getString("qr_code"), rs.getString("email")) },
|
||||||
|
id
|
||||||
|
) ?: throw NotFoundException("Cannot find rent with id %s".format(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun markBookingPayed(rentId: String) {
|
||||||
|
val updatedAmount = template.update {
|
||||||
|
val stmt = it.prepareStatement(MARK_RENT_AS_PAYED_QUERY)
|
||||||
|
stmt.setString(1, rentId)
|
||||||
|
stmt
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedAmount == 0)
|
||||||
|
throw NotFoundException("Cannot find rent with id %s".format(rentId))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteRentByDateCreatedLessThen(dateTime: LocalDateTime): Int {
|
||||||
|
return template.update {
|
||||||
|
val stmt = it.prepareStatement("delete from rents where date_created <= ? and payed = false")
|
||||||
|
stmt.setTimestamp(1, Timestamp.valueOf(dateTime))
|
||||||
|
stmt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package ru.vyatsu.qr_access_api.booking.repository.entity
|
||||||
|
|
||||||
|
data class RentWithEmail(val qrCode: String, val email: String)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package ru.vyatsu.qr_access_api.booking.request
|
||||||
|
|
||||||
|
data class BookCallbackRequest(val rentId: String)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package ru.vyatsu.qr_access_api.booking.request
|
||||||
|
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
data class BookRequest(
|
||||||
|
val startDateTime: LocalDateTime,
|
||||||
|
val endDateTime: LocalDateTime,
|
||||||
|
val doorId: String,
|
||||||
|
val clientEmail: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package ru.vyatsu.qr_access_api.booking.request
|
||||||
|
|
||||||
|
data class BookResponse(val rentId: String)
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package ru.vyatsu.qr_access_api.booking.service
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.node.JsonNodeFactory
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import ru.vyatsu.qr_access_api.booking.repository.BookingRepository
|
||||||
|
import ru.vyatsu.qr_access_api.booking.request.BookRequest
|
||||||
|
import ru.vyatsu.qr_access_api.common.exception.ValidationException
|
||||||
|
import ru.vyatsu.qr_access_api.email.repository.EmailRepository
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class BookingService(val bookingRepository: BookingRepository, val emailRepository: EmailRepository) {
|
||||||
|
fun book(request: BookRequest): String {
|
||||||
|
// TODO: Произвести еще валидации, если нужны
|
||||||
|
val date = request.startDateTime.toLocalDate()
|
||||||
|
if (date != request.endDateTime.toLocalDate()) {
|
||||||
|
throw ValidationException(
|
||||||
|
"startDateTime, endDateTime",
|
||||||
|
"startDateTime and endDateTime have to have the same day"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val startTime = request.startDateTime.toLocalTime()
|
||||||
|
val endTime = request.endDateTime.toLocalTime()
|
||||||
|
|
||||||
|
return bookingRepository.book(request.doorId, date, startTime, endTime, request.clientEmail)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun bookPayed(bookId: String) {
|
||||||
|
bookingRepository.markBookingPayed(bookId)
|
||||||
|
val (qrCode, email) = bookingRepository.findRentWithClientById(bookId)
|
||||||
|
val additionalData = JsonNodeFactory.instance.objectNode()
|
||||||
|
additionalData.set<ObjectNode>("qr_code", JsonNodeFactory.instance.textNode(qrCode))
|
||||||
|
emailRepository.createEmailOutboxRecord(email, "qr_code", additionalData)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package ru.vyatsu.qr_access_api.common
|
||||||
|
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
inline fun <reified T> T.logger(): Logger {
|
||||||
|
return LoggerFactory.getLogger(T::class.java)
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package ru.vyatsu.qr_access_api.common.config
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.mail.javamail.JavaMailSender
|
||||||
|
import org.springframework.mail.javamail.JavaMailSenderImpl
|
||||||
|
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class EmailConfig {
|
||||||
|
@Bean
|
||||||
|
fun mailSender(): JavaMailSender {
|
||||||
|
val mailSender = JavaMailSenderImpl()
|
||||||
|
mailSender.host = "smtp.yandex.ru"
|
||||||
|
mailSender.port = 587
|
||||||
|
mailSender.username = "kashiuno@yandex.ru"
|
||||||
|
mailSender.password = "qmpEMP262049!!!?EEWChaosMeteor"
|
||||||
|
|
||||||
|
val props = mailSender.javaMailProperties
|
||||||
|
props["mail.transport.protocol"] = "smtp"
|
||||||
|
props["mail.smtp.auth"] = "true"
|
||||||
|
props["mail.smtp.starttls.enable"] = "true"
|
||||||
|
props["mail.debug"] = "true"
|
||||||
|
|
||||||
|
return mailSender
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package ru.vyatsu.qr_access_api.common.exception
|
||||||
|
|
||||||
|
class ValidationException(val fieldName: String, override val message: String) :
|
||||||
|
RuntimeException("fieldName: %s -- message: %s".format(fieldName, message))
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package ru.vyatsu.qr_access_api.config
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.http.HttpHeaders
|
||||||
|
import org.springframework.http.HttpMethod
|
||||||
|
import org.springframework.security.config.Customizer
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||||
|
import org.springframework.security.web.SecurityFilterChain
|
||||||
|
import org.springframework.web.cors.CorsConfiguration
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity(debug = true)
|
||||||
|
class SecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun defaultSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||||
|
return http.authorizeHttpRequests {
|
||||||
|
it.requestMatchers("/public/**", "/error").permitAll()
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
}
|
||||||
|
.oauth2ResourceServer { it.jwt(Customizer.withDefaults()) }
|
||||||
|
.cors { c ->
|
||||||
|
c.configurationSource {
|
||||||
|
val config = CorsConfiguration()
|
||||||
|
config.addAllowedOrigin("http://localhost:3000")
|
||||||
|
config.allowedMethods = listOf(
|
||||||
|
HttpMethod.GET.name(),
|
||||||
|
HttpMethod.POST.name()
|
||||||
|
)
|
||||||
|
config.allowedHeaders = listOf(
|
||||||
|
HttpHeaders.CONTENT_TYPE
|
||||||
|
)
|
||||||
|
config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.csrf { c -> c.disable() }
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package ru.vyatsu.qr_access_api.email.job
|
||||||
|
|
||||||
|
import io.nayuki.qrcodegen.QrCode
|
||||||
|
import org.springframework.mail.javamail.JavaMailSender
|
||||||
|
import org.springframework.mail.javamail.MimeMessageHelper
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import ru.vyatsu.qr_access_api.common.logger
|
||||||
|
import ru.vyatsu.qr_access_api.email.repository.EmailRepository
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.util.*
|
||||||
|
import javax.imageio.ImageIO
|
||||||
|
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class EmailOutboxSendJob(val repository: EmailRepository, val mailSender: JavaMailSender) {
|
||||||
|
|
||||||
|
@Scheduled(cron = "*/5 * * * * *")
|
||||||
|
fun sendEmails() {
|
||||||
|
val messagesToSend = repository.findRecordsToSendWithBlock()
|
||||||
|
|
||||||
|
messagesToSend.forEach {
|
||||||
|
val msg = mailSender.createMimeMessage()
|
||||||
|
|
||||||
|
val helper = MimeMessageHelper(msg, true)
|
||||||
|
|
||||||
|
helper.setFrom("kashiuno@yandex.ru")
|
||||||
|
helper.setTo(it.email)
|
||||||
|
helper.setSubject("Приобретение qr-кода")
|
||||||
|
helper.setText("Вы приобрели проход в коворкинг на нашем сайте. qr-код во вложении. QR-код нужно приложить к сканеру соответствующей двери и она откроется")
|
||||||
|
|
||||||
|
val qrCode = it.additionalData.get("qr_code")
|
||||||
|
val code = QrCode.encodeText(qrCode.asText(), QrCode.Ecc.MEDIUM)
|
||||||
|
val image = toImage(code, 3, 2)
|
||||||
|
val os = ByteArrayOutputStream()
|
||||||
|
ImageIO.write(image, "png", os)
|
||||||
|
val imageIS = ByteArrayInputStream(os.toByteArray())
|
||||||
|
helper.addAttachment("qr_code.png") { imageIS }
|
||||||
|
}
|
||||||
|
|
||||||
|
val recordsWasDeletedCount = repository.deleteRecordsToSend(messagesToSend.map { it.id })
|
||||||
|
logger().info("Emails sent {}", recordsWasDeletedCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private fun toImage(qr: QrCode, scale: Int, border: Int, lightColor: Int, darkColor: Int): BufferedImage {
|
||||||
|
Objects.requireNonNull(qr)
|
||||||
|
require(!(scale <= 0 || border < 0)) { "Value out of range" }
|
||||||
|
require(!(border > Int.MAX_VALUE / 2 || qr.size + border * 2L > Int.MAX_VALUE / scale)) { "Scale or border too large" }
|
||||||
|
|
||||||
|
val result = BufferedImage(
|
||||||
|
(qr.size + border * 2) * scale,
|
||||||
|
(qr.size + border * 2) * scale,
|
||||||
|
BufferedImage.TYPE_INT_RGB
|
||||||
|
)
|
||||||
|
for (y in 0 until result.height) {
|
||||||
|
for (x in 0 until result.width) {
|
||||||
|
val color = qr.getModule(x / scale - border, y / scale - border)
|
||||||
|
result.setRGB(x, y, if (color) darkColor else lightColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toImage(qr: QrCode?, scale: Int, border: Int): BufferedImage {
|
||||||
|
return toImage(qr!!, scale, border, 0xFFFFFF, 0x000000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package ru.vyatsu.qr_access_api.email.repository
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import org.springframework.jdbc.core.BatchPreparedStatementSetter
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
import ru.vyatsu.qr_access_api.email.repository.entity.EmailOutbox
|
||||||
|
import java.sql.PreparedStatement
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
private const val INSERT_EMAIL_OUTBOX_RECORD =
|
||||||
|
"insert into email_outbox(id, email, template, additional_info) values (?, ?, ?, (to_json(?::json)))"
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
class EmailRepository(private val template: JdbcTemplate, private val om: ObjectMapper) {
|
||||||
|
fun createEmailOutboxRecord(email: String, msgTemplate: String, additionalInfo: JsonNode) {
|
||||||
|
val insertedCount = template.update {
|
||||||
|
val stmt = it.prepareStatement(INSERT_EMAIL_OUTBOX_RECORD)
|
||||||
|
stmt.setString(1, UUID.randomUUID().toString())
|
||||||
|
stmt.setString(2, email)
|
||||||
|
stmt.setString(3, msgTemplate)
|
||||||
|
|
||||||
|
stmt.setObject(4, om.writeValueAsString(additionalInfo))
|
||||||
|
stmt
|
||||||
|
}
|
||||||
|
|
||||||
|
if (insertedCount != 1)
|
||||||
|
throw RuntimeException("Inserted rows should be equals to 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findRecordsToSendWithBlock(): List<EmailOutbox> {
|
||||||
|
return template.query("select id, email, template, additional_info from email_outbox for update", { rs, _ ->
|
||||||
|
EmailOutbox(
|
||||||
|
rs.getString("id"),
|
||||||
|
rs.getString("email"),
|
||||||
|
rs.getString("template"),
|
||||||
|
om.readTree(rs.getString("additional_info"))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteRecordsToSend(ids: List<String>): Int {
|
||||||
|
val query = "delete from email_outbox where id = ?"
|
||||||
|
return template.batchUpdate(query, object : BatchPreparedStatementSetter {
|
||||||
|
override fun setValues(ps: PreparedStatement, i: Int) {
|
||||||
|
ps.setString(1, ids[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getBatchSize(): Int = ids.size
|
||||||
|
})
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package ru.vyatsu.qr_access_api.email.repository.entity
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode
|
||||||
|
|
||||||
|
data class EmailOutbox(val id: String, val email: String, val template: String, val additionalData: JsonNode)
|
||||||
+4
-2
@@ -1,10 +1,12 @@
|
|||||||
package ru.vyatsu.qr_access_api.controller
|
package ru.vyatsu.qr_access_api.qr.controller
|
||||||
|
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
import ru.vyatsu.apis.QrApi
|
import ru.vyatsu.apis.QrApi
|
||||||
import ru.vyatsu.models.QrCodesResponse
|
import ru.vyatsu.models.QrCodesResponse
|
||||||
import ru.vyatsu.qr_access_api.service.QrSyncService
|
import ru.vyatsu.qr_access_api.qr.service.QrSyncService
|
||||||
|
|
||||||
|
@RestController
|
||||||
class QrSyncController(val syncService: QrSyncService) : QrApi {
|
class QrSyncController(val syncService: QrSyncService) : QrApi {
|
||||||
|
|
||||||
override fun getQrCodes(): ResponseEntity<QrCodesResponse> =
|
override fun getQrCodes(): ResponseEntity<QrCodesResponse> =
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package ru.vyatsu.qr_access_api.controller
|
package ru.vyatsu.qr_access_api.qr.controller
|
||||||
|
|
||||||
import org.springframework.http.HttpStatusCode
|
import org.springframework.http.HttpStatusCode
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package ru.vyatsu.qr_access_api.qr.repository
|
||||||
|
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
import ru.vyatsu.models.QrCode
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
private const val GET_ACTUAL_QRS_BY_UNIT_ID = """
|
||||||
|
SELECT r.start_time, r.end_time, r.date, r.door_id, r.qr_code FROM rents r
|
||||||
|
JOIN doors d ON (d.id = r.door_id)
|
||||||
|
JOIN oauth2_registered_client c ON (c.client_id = d.unit_id)
|
||||||
|
WHERE c.client_id = ? AND r.start_time <= CURRENT_TIMESTAMP AND r.end_time >= CURRENT_TIMESTAMP
|
||||||
|
"""
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
class QrRepository(private val template: JdbcTemplate) {
|
||||||
|
|
||||||
|
fun getActualQrCodesByUnitId(unitId: String): List<QrCode> {
|
||||||
|
val query = GET_ACTUAL_QRS_BY_UNIT_ID.trimIndent()
|
||||||
|
return template.query({ conn ->
|
||||||
|
val stmt = conn.prepareStatement(query)
|
||||||
|
stmt.setString(1, unitId)
|
||||||
|
stmt
|
||||||
|
}, { rs, _ ->
|
||||||
|
QrCode(
|
||||||
|
LocalDateTime.of(rs.getDate("date").toLocalDate(), rs.getTime("start_time").toLocalTime()).atZone(ZoneId.systemDefault()).toOffsetDateTime(),
|
||||||
|
LocalDateTime.of(rs.getDate("date").toLocalDate(), rs.getTime("end_time").toLocalTime()).atZone(ZoneId.systemDefault()).toOffsetDateTime(),
|
||||||
|
UUID.fromString(rs.getString("door_id")),
|
||||||
|
UUID.fromString(rs.getString("qr_code"))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package ru.vyatsu.qr_access_api.qr.service
|
||||||
|
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import ru.vyatsu.models.QrCode
|
||||||
|
import ru.vyatsu.qr_access_api.qr.repository.QrRepository
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class QrSyncService(val qrRepository: QrRepository) {
|
||||||
|
fun getQrCodes(): List<QrCode> {
|
||||||
|
val sc = SecurityContextHolder.getContext()
|
||||||
|
return qrRepository.getActualQrCodesByUnitId(sc.authentication.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package ru.vyatsu.qr_access_api.repository
|
|
||||||
|
|
||||||
import org.springframework.jdbc.core.JdbcTemplate
|
|
||||||
import org.springframework.stereotype.Repository
|
|
||||||
import ru.vyatsu.models.QrCode
|
|
||||||
import java.time.ZoneId
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
private const val GET_ACTUAL_QRS_BY_UNIT_ID = """
|
|
||||||
SELECT q.start_date_time, q.end_date_time, q.door_id, q.key_code FROM qrs q
|
|
||||||
JOIN doors d ON (d.id = q.door_id)
|
|
||||||
JOIN oauth2_authorized_client c ON (c.client_registration_id = d.unit_id AND c.principal_name = d.principal_name)
|
|
||||||
WHERE c.client_registration_id = ? AND q.start_date_time <= CURRENT_TIMESTAMP AND q.end_date_time >= CURRENT_TIMESTAMP
|
|
||||||
"""
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
class QrRepository(private val template: JdbcTemplate) {
|
|
||||||
|
|
||||||
fun getActualQrCodesByUnitId(unitId: String): List<QrCode> {
|
|
||||||
val query = GET_ACTUAL_QRS_BY_UNIT_ID.trimIndent()
|
|
||||||
return template.query({ conn ->
|
|
||||||
val stmt = conn.prepareStatement(query)
|
|
||||||
stmt.setString(1, unitId)
|
|
||||||
stmt
|
|
||||||
}, { rs, _ ->
|
|
||||||
QrCode(
|
|
||||||
rs.getTimestamp("start_date_time").toLocalDateTime().atZone(ZoneId.systemDefault()).toOffsetDateTime(),
|
|
||||||
rs.getTimestamp("end_date_time").toLocalDateTime().atZone(ZoneId.systemDefault()).toOffsetDateTime(),
|
|
||||||
UUID.fromString(rs.getString("door_id")),
|
|
||||||
UUID.fromString(rs.getString("key_code"))
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package ru.vyatsu.qr_access_api.service
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Service
|
|
||||||
import ru.vyatsu.models.QrCode
|
|
||||||
import ru.vyatsu.qr_access_api.repository.QrRepository
|
|
||||||
|
|
||||||
@Service
|
|
||||||
class QrSyncService(val qrRepository: QrRepository) {
|
|
||||||
fun getQrCodes(): List<QrCode> {
|
|
||||||
// TODO: Тут логика с извлечением клайма из jwt в котором идентификатор клиента лежит
|
|
||||||
val extractedUnitId = "945c8621-9adc-4a49-bc56-10253d27c581"
|
|
||||||
return qrRepository.getActualQrCodesByUnitId(extractedUnitId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package ru.vyatsu.qr_access_api.slots.controller
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.*
|
||||||
|
import ru.vyatsu.qr_access_api.slots.response.SlotResponse
|
||||||
|
import ru.vyatsu.qr_access_api.slots.service.SlotService
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("public")
|
||||||
|
class SlotsController(val service: SlotService) {
|
||||||
|
|
||||||
|
@GetMapping("/slots/{partnerId}")
|
||||||
|
fun getSlots(@PathVariable partnerId: String): SlotResponse {
|
||||||
|
return service.getAllSlotsByPartner(partnerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
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_PARTNER_ID =
|
||||||
|
"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 rents 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 findRentDateTimesByPartnerId(partnerId: String): DoorRent {
|
||||||
|
val doors = DoorRent(mutableMapOf())
|
||||||
|
jdbc.query({
|
||||||
|
val stmt = it.prepareStatement(FIND_RENT_DOORS_BY_PARTNER_ID)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package ru.vyatsu.qr_access_api.slots.repository.entity
|
||||||
|
|
||||||
|
data class DoorRent(val dates: MutableMap<String, RentDate>)
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
@@ -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>>)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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>)
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package ru.vyatsu.qr_access_api.slots.response
|
||||||
|
|
||||||
|
data class SlotResponse(val doors: List<Door>)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package ru.vyatsu.qr_access_api.slots.response
|
||||||
|
|
||||||
|
enum class SlotStatus {
|
||||||
|
FREE, BOOKED, OUT_OF_WORKING_TIME
|
||||||
|
}
|
||||||
@@ -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.findRentDateTimesByPartnerId(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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
spring.application.name=qr-access-api
|
|
||||||
spring.datasource.url=jdbc:postgresql://localhost:5432/qr_access
|
|
||||||
spring.datasource.username=qr_access_user
|
|
||||||
spring.datasource.password=123
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: qr-access-api
|
||||||
|
datasource:
|
||||||
|
url: jdbc:postgresql://localhost:5432/qr_access
|
||||||
|
username: qr_access_user
|
||||||
|
password: 123
|
||||||
|
security:
|
||||||
|
oauth2:
|
||||||
|
resourceserver:
|
||||||
|
jwt:
|
||||||
|
jwk-set-uri: http://localhost:8081/oauth2/jwks
|
||||||
@@ -4,63 +4,104 @@ databaseChangeLog:
|
|||||||
author: d.krupin
|
author: d.krupin
|
||||||
changes:
|
changes:
|
||||||
- createTable:
|
- createTable:
|
||||||
tableName: oauth2_authorized_client
|
tableName: partners
|
||||||
|
columns:
|
||||||
|
- column:
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
primaryKey: true
|
||||||
|
primaryKeyName: PK_partners
|
||||||
|
name: id
|
||||||
|
type: TEXT
|
||||||
|
- column:
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
name: name
|
||||||
|
type: TEXT
|
||||||
|
- createTable:
|
||||||
|
tableName: oauth2_registered_client
|
||||||
columns:
|
columns:
|
||||||
- column:
|
- column:
|
||||||
constraints:
|
constraints:
|
||||||
nullable: false
|
nullable: false
|
||||||
primaryKey: true
|
primaryKey: true
|
||||||
primaryKeyName: PK_oauth2_client
|
primaryKeyName: PK_oauth2_client
|
||||||
name: client_registration_id
|
name: id
|
||||||
type: VARCHAR(100)
|
type: VARCHAR(100)
|
||||||
- column:
|
- column:
|
||||||
constraints:
|
constraints:
|
||||||
nullable: false
|
nullable: false
|
||||||
primaryKey: true
|
unique: true
|
||||||
primaryKeyName: PK_oauth2_client
|
name: client_id
|
||||||
name: principal_name
|
|
||||||
type: VARCHAR(200)
|
|
||||||
- column:
|
|
||||||
name: access_token_type
|
|
||||||
type: VARCHAR(100)
|
type: VARCHAR(100)
|
||||||
|
- column:
|
||||||
|
name: client_id_issued_at
|
||||||
|
type: TIMESTAMP
|
||||||
constraints:
|
constraints:
|
||||||
nullable: false
|
nullable: false
|
||||||
- column:
|
- column:
|
||||||
name: access_token_value
|
name: client_secret
|
||||||
type: TEXT
|
type: TEXT
|
||||||
constraints:
|
constraints:
|
||||||
nullable: false
|
nullable: false
|
||||||
- column:
|
- column:
|
||||||
name: access_token_issued_at
|
name: client_secret_expires_at
|
||||||
type: TIMESTAMP
|
type: TIMESTAMP
|
||||||
constraints:
|
constraints:
|
||||||
nullable: false
|
nullable: false
|
||||||
- column:
|
- column:
|
||||||
name: access_token_expires_at
|
name: client_name
|
||||||
type: TIMESTAMP
|
type: TEXT
|
||||||
constraints:
|
constraints:
|
||||||
nullable: false
|
nullable: false
|
||||||
- column:
|
- column:
|
||||||
name: access_token_scopes
|
name: client_authentication_methods
|
||||||
|
type: TEXT
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
- column:
|
||||||
|
name: authorization_grant_types
|
||||||
type: TEXT
|
type: TEXT
|
||||||
constraints:
|
constraints:
|
||||||
nullable: true
|
nullable: true
|
||||||
- column:
|
- column:
|
||||||
name: refresh_token_value
|
name: redirect_uris
|
||||||
type: TEXT
|
type: TEXT
|
||||||
constraints:
|
constraints:
|
||||||
nullable: true
|
nullable: true
|
||||||
- column:
|
- column:
|
||||||
name: refresh_token_issued_at
|
name: post_logout_redirect_uris
|
||||||
type: TIMESTAMP
|
type: TEXT
|
||||||
constraints:
|
|
||||||
nullable: true
|
|
||||||
- column:
|
|
||||||
name: created_at
|
|
||||||
type: TIMESTAMP
|
|
||||||
constraints:
|
constraints:
|
||||||
nullable: false
|
nullable: false
|
||||||
defaultValueComputed: CURRENT_TIMESTAMP
|
- column:
|
||||||
|
name: scopes
|
||||||
|
type: TEXT
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
- column:
|
||||||
|
name: client_settings
|
||||||
|
type: TEXT
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
- column:
|
||||||
|
name: token_settings
|
||||||
|
type: TEXT
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
- column:
|
||||||
|
name: admin_editable
|
||||||
|
type: BOOLEAN
|
||||||
|
defaultValue: TRUE
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
- column:
|
||||||
|
name: partner_id
|
||||||
|
type: TEXT
|
||||||
|
constraints:
|
||||||
|
nullable: true
|
||||||
|
foreignKeyName: FK_units_partners
|
||||||
|
references: partners(id)
|
||||||
- createTable:
|
- createTable:
|
||||||
tableName: doors
|
tableName: doors
|
||||||
columns:
|
columns:
|
||||||
@@ -77,40 +118,205 @@ databaseChangeLog:
|
|||||||
name: unit_id
|
name: unit_id
|
||||||
type: VARCHAR(100)
|
type: VARCHAR(100)
|
||||||
- column:
|
- column:
|
||||||
|
constraints:
|
||||||
|
nullable: true
|
||||||
|
name: description
|
||||||
|
type: TEXT
|
||||||
|
- column:
|
||||||
|
name: count
|
||||||
|
type: INT
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
- column:
|
||||||
|
name: parent_door_ids
|
||||||
|
type: TEXT
|
||||||
|
constraints:
|
||||||
|
nullable: true
|
||||||
|
- column:
|
||||||
|
name: price
|
||||||
|
type: DECIMAL(12, 2)
|
||||||
constraints:
|
constraints:
|
||||||
nullable: false
|
nullable: false
|
||||||
name: principal_name
|
|
||||||
type: VARCHAR(200)
|
|
||||||
- createTable:
|
- createTable:
|
||||||
tableName: qrs
|
tableName: clients
|
||||||
columns:
|
columns:
|
||||||
- column:
|
- column:
|
||||||
constraints:
|
constraints:
|
||||||
nullable: false
|
nullable: false
|
||||||
primaryKey: true
|
primaryKey: true
|
||||||
primaryKeyName: PK_qrs
|
primaryKeyName: PK_clients
|
||||||
name: key_code
|
name: id
|
||||||
type: TEXT
|
type: TEXT
|
||||||
- column:
|
- column:
|
||||||
constraints:
|
constraints:
|
||||||
nullable: false
|
nullable: false
|
||||||
foreignKeyName: fk_qr_door
|
unique: true
|
||||||
references: doors(id)
|
name: email
|
||||||
name: door_id
|
type: TEXT
|
||||||
|
- column:
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
name: email_is_confirmed
|
||||||
|
type: BOOLEAN
|
||||||
|
- createTable:
|
||||||
|
tableName: rents
|
||||||
|
columns:
|
||||||
|
- column:
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
primaryKey: true
|
||||||
|
primaryKeyName: PK_rent
|
||||||
|
name: id
|
||||||
type: TEXT
|
type: TEXT
|
||||||
- column:
|
- column:
|
||||||
constraints:
|
constraints:
|
||||||
nullable: true
|
nullable: true
|
||||||
name: start_date_time
|
name: start_time
|
||||||
type: TIMESTAMP WITH TIME ZONE
|
type: TIME WITH TIME ZONE
|
||||||
- column:
|
- column:
|
||||||
constraints:
|
constraints:
|
||||||
nullable: true
|
nullable: true
|
||||||
name: end_date_time
|
name: end_time
|
||||||
|
type: TIME WITH TIME ZONE
|
||||||
|
- column:
|
||||||
|
name: client_id
|
||||||
|
type: TEXT
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
foreignKeyName: FK_rent_clients
|
||||||
|
references: clients(id)
|
||||||
|
- column:
|
||||||
|
name: date
|
||||||
|
type: DATE
|
||||||
|
constraints:
|
||||||
|
nullable: true
|
||||||
|
- column:
|
||||||
|
name: door_id
|
||||||
|
type: TEXT
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
foreignKeyName: FK_rent_doors
|
||||||
|
references: doors(id)
|
||||||
|
- column:
|
||||||
|
name: qr_code
|
||||||
|
type: TEXT
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
- column:
|
||||||
|
name: payed
|
||||||
|
type: BOOLEAN
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
- column:
|
||||||
|
name: date_created
|
||||||
type: TIMESTAMP WITH TIME ZONE
|
type: TIMESTAMP WITH TIME ZONE
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
- createTable:
|
||||||
|
tableName: schedule
|
||||||
|
columns:
|
||||||
|
- column:
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
primaryKey: true
|
||||||
|
primaryKeyName: PK_schedule_doors
|
||||||
|
name: id
|
||||||
|
type: TEXT
|
||||||
|
- column:
|
||||||
|
name: door_id
|
||||||
|
type: TEXT
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
foreignKeyName: FK_schedule_doors
|
||||||
|
references: doors(id)
|
||||||
|
- column:
|
||||||
|
constraints:
|
||||||
|
nullable: true
|
||||||
|
name: start_time
|
||||||
|
type: TIME WITH TIME ZONE
|
||||||
|
- column:
|
||||||
|
constraints:
|
||||||
|
nullable: true
|
||||||
|
name: end_time
|
||||||
|
type: TIME WITH TIME ZONE
|
||||||
|
- column:
|
||||||
|
name: date
|
||||||
|
type: DATE
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
- createTable:
|
||||||
|
tableName: email_outbox
|
||||||
|
columns:
|
||||||
|
- column:
|
||||||
|
name: id
|
||||||
|
type: TEXT
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
primaryKey: true
|
||||||
|
primaryKeyName: PK_email_outbox
|
||||||
|
- column:
|
||||||
|
name: email
|
||||||
|
type: TEXT
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
- column:
|
||||||
|
name: template
|
||||||
|
type: TEXT
|
||||||
|
constraints:
|
||||||
|
nullable: false
|
||||||
|
- column:
|
||||||
|
name: additional_info
|
||||||
|
type: JSONB
|
||||||
|
constraints:
|
||||||
|
nullable: true
|
||||||
|
- insert:
|
||||||
|
tableName: oauth2_registered_client
|
||||||
|
columns:
|
||||||
|
- column:
|
||||||
|
name: id
|
||||||
|
value: f1434eb3-7c38-45c2-9e21-f1917157ffb7
|
||||||
|
- column:
|
||||||
|
name: client_id
|
||||||
|
value: admin
|
||||||
|
- column:
|
||||||
|
name: client_id_issued_at
|
||||||
|
valueComputed: CURRENT_TIMESTAMP
|
||||||
|
- column:
|
||||||
|
name: client_secret
|
||||||
|
value: '{bcrypt}$2a$12$ihzZm/AsJCAYjoII9hd1IO25xpmHhnsOnaSuvfTPuMgt45w7cFNXi'
|
||||||
|
- column:
|
||||||
|
name: client_secret_expires_at
|
||||||
|
value: '2030-06-02T12:00:00'
|
||||||
|
- column:
|
||||||
|
name: client_name
|
||||||
|
value: admin-client
|
||||||
|
- column:
|
||||||
|
name: client_authentication_methods
|
||||||
|
value: none
|
||||||
|
- column:
|
||||||
|
name: authorization_grant_types
|
||||||
|
value: refresh_token,authorization_code
|
||||||
|
- column:
|
||||||
|
name: redirect_uris
|
||||||
|
value: 'http://localhost:8082/login/oauth2/code/own'
|
||||||
|
- column:
|
||||||
|
name: post_logout_redirect_uris
|
||||||
|
value: 'http://localhost:8082/units'
|
||||||
|
- column:
|
||||||
|
name: scopes
|
||||||
|
value: 'admin,openid'
|
||||||
|
- column:
|
||||||
|
name: client_settings
|
||||||
|
value: '{"@class":"java.util.Collections$UnmodifiableMap","settings.client.require-proof-key":true,"settings.client.require-authorization-consent":false}'
|
||||||
|
- column:
|
||||||
|
name: token_settings
|
||||||
|
value: '{"@class":"java.util.Collections$UnmodifiableMap","settings.token.reuse-refresh-tokens":false,"settings.token.x509-certificate-bound-access-tokens":false,"settings.token.id-token-signature-algorithm":["org.springframework.security.oauth2.jose.jws.SignatureAlgorithm","RS256"],"settings.token.access-token-time-to-live":["java.time.Duration",600.000000000],"settings.token.access-token-format":{"@class":"org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat","value":"self-contained"},"settings.token.refresh-token-time-to-live":["java.time.Duration",600.000000000],"settings.token.authorization-code-time-to-live":["java.time.Duration",300.000000000],"settings.token.device-code-time-to-live":["java.time.Duration",300.000000000]}'
|
||||||
|
- column:
|
||||||
|
name: admin_editable
|
||||||
|
value: 'FALSE'
|
||||||
- addForeignKeyConstraint:
|
- addForeignKeyConstraint:
|
||||||
baseColumnNames: unit_id, principal_name
|
baseColumnNames: unit_id
|
||||||
baseTableName: doors
|
baseTableName: doors
|
||||||
constraintName: FK_unit_door
|
constraintName: FK_unit_door
|
||||||
referencedColumnNames: client_registration_id, principal_name
|
referencedColumnNames: client_id
|
||||||
referencedTableName: oauth2_authorized_client
|
referencedTableName: oauth2_registered_client
|
||||||
@@ -5,8 +5,8 @@ import java.time.LocalDateTime
|
|||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
private const val INSERT_CLIENT_QUERY =
|
private const val INSERT_CLIENT_QUERY =
|
||||||
"""INSERT INTO oauth2_authorized_client(client_registration_id, principal_name, access_token_type, access_token_value, access_token_issued_at, access_token_expires_at, created_at)
|
"""INSERT INTO oauth2_registered_client(id, client_id, client_id_issued_at, client_secret, client_secret_expires_at, client_name, client_authentication_methods, authorization_grant_types, redirect_uris, post_logout_redirect_uris, scopes, client_settings, token_settings)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)"""
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, to_json(?::json), to_json(?::json))"""
|
||||||
|
|
||||||
class InsertDatabaseHelper(private val template: JdbcTemplate) {
|
class InsertDatabaseHelper(private val template: JdbcTemplate) {
|
||||||
fun insertQr(
|
fun insertQr(
|
||||||
@@ -23,23 +23,28 @@ class InsertDatabaseHelper(private val template: JdbcTemplate) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun insertDoor(id: String, unitId: String, principalName: String = ""): Int {
|
fun insertDoor(id: String, unitId: String): Int {
|
||||||
return template.update("INSERT INTO doors(id, unit_id, principal_name) VALUES (?, ?, ?)") { ps ->
|
return template.update("INSERT INTO doors(id, unit_id) VALUES (?, ?)") { ps ->
|
||||||
ps.setString(1, id)
|
ps.setString(1, id)
|
||||||
ps.setString(2, unitId)
|
ps.setString(2, unitId)
|
||||||
ps.setString(3, principalName)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun insertClient(id: String, principalName: String = ""): Int {
|
fun insertClient(id: String): Int {
|
||||||
return template.update(INSERT_CLIENT_QUERY) { ps ->
|
return template.update(INSERT_CLIENT_QUERY) { ps ->
|
||||||
ps.setString(1, id)
|
ps.setString(1, id)
|
||||||
ps.setString(2, principalName)
|
ps.setString(2, id)
|
||||||
ps.setString(3, "Bearer")
|
ps.setObject(3, LocalDateTime.now())
|
||||||
ps.setString(4, "Tokenasfgerseawvg")
|
ps.setString(4, "secret")
|
||||||
ps.setObject(5, LocalDateTime.now())
|
ps.setObject(5, LocalDateTime.now())
|
||||||
ps.setObject(6, LocalDateTime.now())
|
ps.setString(6, id)
|
||||||
ps.setObject(7, LocalDateTime.now())
|
ps.setString(7, "client_secret_post")
|
||||||
|
ps.setString(8, "client_credentials")
|
||||||
|
ps.setString(9, "http://localhost:8080")
|
||||||
|
ps.setString(10, "http://localhost:8080")
|
||||||
|
ps.setString(11, "")
|
||||||
|
ps.setString(12, "{}")
|
||||||
|
ps.setString(13, "{}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import org.springframework.context.annotation.Bean
|
|||||||
import org.springframework.context.annotation.Import
|
import org.springframework.context.annotation.Import
|
||||||
import org.springframework.jdbc.core.JdbcTemplate
|
import org.springframework.jdbc.core.JdbcTemplate
|
||||||
import ru.vyatsu.qr_access_api.database.utils.InsertDatabaseHelper
|
import ru.vyatsu.qr_access_api.database.utils.InsertDatabaseHelper
|
||||||
|
import ru.vyatsu.qr_access_api.qr.repository.QrRepository
|
||||||
|
|
||||||
@JdbcTest
|
@JdbcTest
|
||||||
@Import(RepositoryTest.Configuration::class)
|
@Import(RepositoryTest.Configuration::class)
|
||||||
|
|||||||
Reference in New Issue
Block a user