Compare commits

..

6 Commits

Author SHA1 Message Date
server_main 1bacf534b9 refactor: better SerializersModule structure 2026-08-18 10:40:18 +00:00
server_main 77cf75a2be V1.0.0
All backend function is available
2026-08-18 09:20:39 +00:00
SOR dd660d416a Merge branch 'backend-dev'
# Conflicts:
#	src/backend/src/main/kotlin/club/itstudio/itmanager/service/impl/FinanceRecordServiceImpl.kt
#	src/backend/src/main/kotlin/club/itstudio/itmanager/service/impl/ItemServiceImpl.kt
2026-08-18 17:05:13 +08:00
SOR 5d3a727b73 fix: page query wrapper 2026-08-18 16:58:32 +08:00
SOR2171 4f640b134a Merge pull request 'fix: page query' (#9) from backend-dev into main
Reviewed-on: #9
2026-08-18 16:05:41 +08:00
SOR 36d1b94fd8 fix: page query 2026-08-18 16:05:17 +08:00
16 changed files with 296 additions and 243 deletions
+2 -1
View File
@@ -7,7 +7,7 @@ plugins {
} }
group = "club.itstudio" group = "club.itstudio"
version = "0.1.1" version = "1.0.0"
description = "itmanager" description = "itmanager"
java { java {
@@ -23,6 +23,7 @@ repositories {
dependencies { dependencies {
implementation("com.auth0:java-jwt:4.4.0") implementation("com.auth0:java-jwt:4.4.0")
implementation("com.baomidou:mybatis-plus-spring-boot4-starter:3.5.15") implementation("com.baomidou:mybatis-plus-spring-boot4-starter:3.5.15")
implementation("com.baomidou:mybatis-plus-jsqlparser:3.5.15")
implementation("org.springframework.boot:spring-boot-starter-amqp") implementation("org.springframework.boot:spring-boot-starter-amqp")
implementation("org.springframework.boot:spring-boot-starter-data-redis-reactive") implementation("org.springframework.boot:spring-boot-starter-data-redis-reactive")
implementation("org.springframework.boot:spring-boot-starter-json") implementation("org.springframework.boot:spring-boot-starter-json")
@@ -0,0 +1,18 @@
package club.itstudio.itmanager.config
import com.baomidou.mybatisplus.annotation.DbType
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class MybatisConfig {
@Bean
fun mybatisPlusInterceptor(): MybatisPlusInterceptor {
val interceptor = MybatisPlusInterceptor()
interceptor.addInnerInterceptor(PaginationInnerInterceptor(DbType.MARIADB))
return interceptor
}
}
@@ -1,7 +1,9 @@
package club.itstudio.itmanager.config package club.itstudio.itmanager.config
import club.itstudio.itmanager.utils.customSerializersModule import club.itstudio.itmanager.utils.BigDecimalSerializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.contextual
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
@@ -10,7 +12,9 @@ class SerializationConfig {
@Bean @Bean
fun kotlinxSerializationJson(): Json = Json { fun kotlinxSerializationJson(): Json = Json {
serializersModule = customSerializersModule serializersModule = SerializersModule {
contextual(BigDecimalSerializer)
}
ignoreUnknownKeys = true ignoreUnknownKeys = true
isLenient = true isLenient = true
encodeDefaults = false encodeDefaults = false
@@ -0,0 +1,61 @@
package club.itstudio.itmanager.service
import club.itstudio.itmanager.utils.Const
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.codec.multipart.FilePart
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.*
@Service
class FileStorageService(
@Value($$"${app.file.data-path}")
private val dataPath: String
) {
data class StoredFile(
val path: Path,
val size: Long
)
private val logger = LoggerFactory.getLogger(this::class.java)
fun saveImage(file: FilePart, subDir: String, entityId: Long): Mono<StoredFile> {
val directory = Paths.get(dataPath, Const.FILE_DIR_IMAGE, subDir, entityId.toString())
val savedName = generateSafeFileName(file.filename())
val target = directory.resolve(savedName)
return Mono.fromCallable<Void> { Files.createDirectories(directory); null }
.then(file.transferTo(target))
.then(Mono.fromCallable { StoredFile(target, Files.size(target)) })
.subscribeOn(Schedulers.boundedElastic())
}
fun deleteFile(path: String?): Mono<Void> {
if (path.isNullOrBlank()) return Mono.empty()
return Mono.fromRunnable<Void> {
try {
Files.deleteIfExists(Paths.get(path))
} catch (e: IOException) {
logger.warn("Failed to delete file: {}", path, e)
}
}.subscribeOn(Schedulers.boundedElastic())
}
fun generateSafeFileName(original: String?): String {
val extension = original
?.substringAfterLast('.')
?.lowercase()
?.takeIf {
it.isNotBlank() &&
it.length <= 10 &&
it.all { ch -> ch.isLetterOrDigit() }
}
return if (extension != null) "${UUID.randomUUID()}.$extension" else UUID.randomUUID().toString()
}
}
@@ -50,7 +50,9 @@ class AccountServiceImpl : ServiceImpl<AccountMapper, Account>(), AccountService
override fun findAccountById(id: Long): Mono<Account> = override fun findAccountById(id: Long): Mono<Account> =
blockingCall { blockingCall {
logger.info("Searching account by id") logger.info("Searching account by id")
ktQuery().eq(Account::id, id).one() ktQuery()
.eq(Account::id, id)
.one()
} }
override fun findAccountByEmail(email: String): Mono<Account> = override fun findAccountByEmail(email: String): Mono<Account> =
@@ -8,7 +8,6 @@ import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers import reactor.core.scheduler.Schedulers
import java.util.NoSuchElementException
@Service @Service
class AttachmentServiceImpl : ServiceImpl<AttachmentMapper, Attachment>(), AttachmentService { class AttachmentServiceImpl : ServiceImpl<AttachmentMapper, Attachment>(), AttachmentService {
@@ -21,19 +20,26 @@ class AttachmentServiceImpl : ServiceImpl<AttachmentMapper, Attachment>(), Attac
override fun getAttachmentById(id: Long): Mono<Attachment> = override fun getAttachmentById(id: Long): Mono<Attachment> =
blockingCall { blockingCall {
logger.info("Searching attachment by id") logger.info("Searching attachment by id")
ktQuery().eq(Attachment::id, id).one() ktQuery()
.eq(Attachment::id, id)
.one()
}.switchIfEmpty(Mono.error(NoSuchElementException("Attachment not found"))) }.switchIfEmpty(Mono.error(NoSuchElementException("Attachment not found")))
override fun listByRelated(relatedTable: String, relatedId: Long): Mono<List<Attachment>> = override fun listByRelated(relatedTable: String, relatedId: Long): Mono<List<Attachment>> =
blockingCall { blockingCall {
logger.info("Searching attachments by related table and id") logger.info("Searching attachments by related table and id")
ktQuery().eq(Attachment::relatedTable, relatedTable).eq(Attachment::relatedId, relatedId).list() ktQuery()
.eq(Attachment::relatedTable, relatedTable)
.eq(Attachment::relatedId, relatedId)
.list()
} }
override fun listByUploader(uploadedBy: Long): Mono<List<Attachment>> = override fun listByUploader(uploadedBy: Long): Mono<List<Attachment>> =
blockingCall { blockingCall {
logger.info("Searching attachments by uploader") logger.info("Searching attachments by uploader")
ktQuery().eq(Attachment::uploadedBy, uploadedBy).list() ktQuery()
.eq(Attachment::uploadedBy, uploadedBy)
.list()
} }
override fun createAttachment(attachment: Attachment): Mono<Boolean> = override fun createAttachment(attachment: Attachment): Mono<Boolean> =
@@ -57,7 +63,11 @@ class AttachmentServiceImpl : ServiceImpl<AttachmentMapper, Attachment>(), Attac
override fun deleteByRelated(relatedTable: String, relatedId: Long): Mono<Boolean> = override fun deleteByRelated(relatedTable: String, relatedId: Long): Mono<Boolean> =
blockingCall { blockingCall {
logger.info("Deleting attachments by related table and id") logger.info("Deleting attachments by related table and id")
remove(ktQuery().eq(Attachment::relatedTable, relatedTable).eq(Attachment::relatedId, relatedId)) remove(
ktQuery()
.eq(Attachment::relatedTable, relatedTable)
.eq(Attachment::relatedId, relatedId)
)
} }
override fun exportAllAttachments(): Mono<List<Attachment>> = override fun exportAllAttachments(): Mono<List<Attachment>> =
@@ -3,13 +3,14 @@ package club.itstudio.itmanager.service.impl
import club.itstudio.itmanager.entity.dto.BorrowRecord import club.itstudio.itmanager.entity.dto.BorrowRecord
import club.itstudio.itmanager.mapper.BorrowRecordMapper import club.itstudio.itmanager.mapper.BorrowRecordMapper
import club.itstudio.itmanager.service.BorrowRecordService import club.itstudio.itmanager.service.BorrowRecordService
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper
import com.baomidou.mybatisplus.core.metadata.OrderItem
import com.baomidou.mybatisplus.extension.plugins.pagination.Page import com.baomidou.mybatisplus.extension.plugins.pagination.Page
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers import reactor.core.scheduler.Schedulers
import java.util.NoSuchElementException
@Service @Service
class BorrowRecordServiceImpl : ServiceImpl<BorrowRecordMapper, BorrowRecord>(), BorrowRecordService { class BorrowRecordServiceImpl : ServiceImpl<BorrowRecordMapper, BorrowRecord>(), BorrowRecordService {
@@ -22,7 +23,9 @@ class BorrowRecordServiceImpl : ServiceImpl<BorrowRecordMapper, BorrowRecord>(),
override fun getBorrowRecordById(id: Long): Mono<BorrowRecord> = override fun getBorrowRecordById(id: Long): Mono<BorrowRecord> =
blockingCall { blockingCall {
logger.info("Searching borrow record by id") logger.info("Searching borrow record by id")
ktQuery().eq(BorrowRecord::id, id).one() ktQuery()
.eq(BorrowRecord::id, id)
.one()
}.switchIfEmpty(Mono.error(NoSuchElementException("BorrowRecord not found"))) }.switchIfEmpty(Mono.error(NoSuchElementException("BorrowRecord not found")))
override fun pageUsages( override fun pageUsages(
@@ -32,39 +35,52 @@ class BorrowRecordServiceImpl : ServiceImpl<BorrowRecordMapper, BorrowRecord>(),
): Mono<Page<BorrowRecord>> = ): Mono<Page<BorrowRecord>> =
blockingCall { blockingCall {
logger.info("Paging borrow records") logger.info("Paging borrow records")
val wrapper = ktQuery() if (keyword.isNullOrBlank() && status.isNullOrBlank()) {
if (!keyword.isNullOrBlank()) { page.addOrder(OrderItem.desc("created_at"))
wrapper.like(BorrowRecord::borrower, keyword) page(page)
} else {
val wrapper = QueryWrapper<BorrowRecord>()
if (!keyword.isNullOrBlank()) {
wrapper.like("borrower", keyword)
}
if (!status.isNullOrBlank()) {
wrapper.eq("status", status)
}
wrapper.orderByDesc("created_at")
page(page, wrapper)
} }
if (!status.isNullOrBlank()) {
wrapper.eq(BorrowRecord::status, status)
}
wrapper.orderByDesc(BorrowRecord::createdAt)
page(page, wrapper)
} }
override fun listByItem(itemId: Long): Mono<List<BorrowRecord>> = override fun listByItem(itemId: Long): Mono<List<BorrowRecord>> =
blockingCall { blockingCall {
logger.info("Searching borrow records by item id") logger.info("Searching borrow records by item id")
ktQuery().eq(BorrowRecord::itemId, itemId).list() ktQuery()
.eq(BorrowRecord::itemId, itemId)
.list()
} }
override fun listByBorrower(borrower: String): Mono<List<BorrowRecord>> = override fun listByBorrower(borrower: String): Mono<List<BorrowRecord>> =
blockingCall { blockingCall {
logger.info("Searching borrow records by borrower") logger.info("Searching borrow records by borrower")
ktQuery().eq(BorrowRecord::borrower, borrower).list() ktQuery()
.eq(BorrowRecord::borrower, borrower)
.list()
} }
override fun listByStatus(status: String): Mono<List<BorrowRecord>> = override fun listByStatus(status: String): Mono<List<BorrowRecord>> =
blockingCall { blockingCall {
logger.info("Searching borrow records by status") logger.info("Searching borrow records by status")
ktQuery().eq(BorrowRecord::status, status).list() ktQuery()
.eq(BorrowRecord::status, status)
.list()
} }
override fun listOverdueRecords(): Mono<List<BorrowRecord>> = override fun listOverdueRecords(): Mono<List<BorrowRecord>> =
blockingCall { blockingCall {
logger.info("Searching overdue borrow records") logger.info("Searching overdue borrow records")
ktQuery().eq(BorrowRecord::status, "OVERDUE").list() ktQuery()
.eq(BorrowRecord::status, "OVERDUE")
.list()
} }
override fun createBorrowRecord(record: BorrowRecord): Mono<Boolean> = override fun createBorrowRecord(record: BorrowRecord): Mono<Boolean> =
@@ -8,7 +8,6 @@ import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers import reactor.core.scheduler.Schedulers
import java.util.NoSuchElementException
@Service @Service
class CategoryServiceImpl : ServiceImpl<CategoryMapper, Category>(), CategoryService { class CategoryServiceImpl : ServiceImpl<CategoryMapper, Category>(), CategoryService {
@@ -21,7 +20,9 @@ class CategoryServiceImpl : ServiceImpl<CategoryMapper, Category>(), CategorySer
override fun getCategoryById(id: Long): Mono<Category> = override fun getCategoryById(id: Long): Mono<Category> =
blockingCall { blockingCall {
logger.info("Searching category by id") logger.info("Searching category by id")
ktQuery().eq(Category::id, id).one() ktQuery()
.eq(Category::id, id)
.one()
}.switchIfEmpty(Mono.error(NoSuchElementException("Category not found"))) }.switchIfEmpty(Mono.error(NoSuchElementException("Category not found")))
override fun listAllCategories(): Mono<List<Category>> = override fun listAllCategories(): Mono<List<Category>> =
@@ -33,7 +34,9 @@ class CategoryServiceImpl : ServiceImpl<CategoryMapper, Category>(), CategorySer
override fun listByParentId(parentId: Long): Mono<List<Category>> = override fun listByParentId(parentId: Long): Mono<List<Category>> =
blockingCall { blockingCall {
logger.info("Searching categories by parent id") logger.info("Searching categories by parent id")
ktQuery().eq(Category::parentId, parentId).list() ktQuery()
.eq(Category::parentId, parentId)
.list()
} }
override fun createCategory(category: Category): Mono<Boolean> = override fun createCategory(category: Category): Mono<Boolean> =
@@ -8,7 +8,6 @@ import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers import reactor.core.scheduler.Schedulers
import java.util.NoSuchElementException
@Service @Service
class EmailNoticeServiceImpl : ServiceImpl<EmailNoticeMapper, EmailNotice>(), EmailNoticeService { class EmailNoticeServiceImpl : ServiceImpl<EmailNoticeMapper, EmailNotice>(), EmailNoticeService {
@@ -21,7 +20,9 @@ class EmailNoticeServiceImpl : ServiceImpl<EmailNoticeMapper, EmailNotice>(), Em
override fun getEmailNoticeById(id: Long): Mono<EmailNotice> = override fun getEmailNoticeById(id: Long): Mono<EmailNotice> =
blockingCall { blockingCall {
logger.info("Searching email notice by id") logger.info("Searching email notice by id")
ktQuery().eq(EmailNotice::id, id).one() ktQuery()
.eq(EmailNotice::id, id)
.one()
}.switchIfEmpty(Mono.error(NoSuchElementException("EmailNotice not found"))) }.switchIfEmpty(Mono.error(NoSuchElementException("EmailNotice not found")))
override fun listAllNotices(): Mono<List<EmailNotice>> = override fun listAllNotices(): Mono<List<EmailNotice>> =
@@ -33,7 +34,9 @@ class EmailNoticeServiceImpl : ServiceImpl<EmailNoticeMapper, EmailNotice>(), Em
override fun listEnabledNotices(): Mono<List<EmailNotice>> = override fun listEnabledNotices(): Mono<List<EmailNotice>> =
blockingCall { blockingCall {
logger.info("Listing enabled email notices") logger.info("Listing enabled email notices")
ktQuery().eq(EmailNotice::enabled, true).list() ktQuery()
.eq(EmailNotice::enabled, true)
.list()
} }
override fun createEmailNotice(notice: EmailNotice): Mono<Boolean> = override fun createEmailNotice(notice: EmailNotice): Mono<Boolean> =
@@ -57,11 +60,8 @@ class EmailNoticeServiceImpl : ServiceImpl<EmailNoticeMapper, EmailNotice>(), Em
override fun toggleEnabled(id: Long): Mono<Boolean> = override fun toggleEnabled(id: Long): Mono<Boolean> =
blockingCall { blockingCall {
val notice = ktQuery().eq(EmailNotice::id, id).one() val notice = ktQuery().eq(EmailNotice::id, id).one()
if (notice != null) { notice != null
updateById(notice.copy(enabled = !notice.enabled)) && updateById(notice.copy(enabled = !notice.enabled))
} else {
false
}
} }
override fun exportAllEmailNotices(): Mono<List<EmailNotice>> = override fun exportAllEmailNotices(): Mono<List<EmailNotice>> =
@@ -3,7 +3,7 @@ package club.itstudio.itmanager.service.impl
import club.itstudio.itmanager.entity.dto.FinanceRecord import club.itstudio.itmanager.entity.dto.FinanceRecord
import club.itstudio.itmanager.mapper.FinanceRecordMapper import club.itstudio.itmanager.mapper.FinanceRecordMapper
import club.itstudio.itmanager.service.FinanceRecordService import club.itstudio.itmanager.service.FinanceRecordService
import com.baomidou.mybatisplus.extension.kotlin.KtQueryWrapper import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper
import com.baomidou.mybatisplus.extension.plugins.pagination.Page import com.baomidou.mybatisplus.extension.plugins.pagination.Page
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -22,7 +22,9 @@ class FinanceRecordServiceImpl : ServiceImpl<FinanceRecordMapper, FinanceRecord>
override fun getFinanceRecordById(id: Long): Mono<FinanceRecord> = override fun getFinanceRecordById(id: Long): Mono<FinanceRecord> =
blockingCall { blockingCall {
logger.info("Searching finance record by id") logger.info("Searching finance record by id")
ktQuery().eq(FinanceRecord::id, id).one() ktQuery()
.eq(FinanceRecord::id, id)
.one()
}.switchIfEmpty(Mono.error(NoSuchElementException("FinanceRecord not found"))) }.switchIfEmpty(Mono.error(NoSuchElementException("FinanceRecord not found")))
override fun pageFinanceRecords( override fun pageFinanceRecords(
@@ -36,55 +38,69 @@ class FinanceRecordServiceImpl : ServiceImpl<FinanceRecordMapper, FinanceRecord>
): Mono<Page<FinanceRecord>> = ): Mono<Page<FinanceRecord>> =
blockingCall { blockingCall {
logger.info("Paging finance records") logger.info("Paging finance records")
val wrapper = KtQueryWrapper(FinanceRecord()) if (keyword.isNullOrBlank() && type.isNullOrBlank() && start.isNullOrBlank() &&
if (!keyword.isNullOrBlank()) { end.isNullOrBlank() && vendor.isNullOrBlank() && itemId == null
wrapper.and { ) {
it.like(FinanceRecord::recordNo, keyword) page(page)
.or().like(FinanceRecord::vendor, keyword) } else {
.or().like(FinanceRecord::note, keyword) val wrapper = QueryWrapper<FinanceRecord>()
if (!keyword.isNullOrBlank()) {
wrapper.and {
it.like("record_no", keyword)
.or().like("vendor", keyword)
.or().like("note", keyword)
}
} }
if (!type.isNullOrBlank()) {
wrapper.eq("type", type)
}
if (!start.isNullOrBlank() && !end.isNullOrBlank()) {
wrapper.apply("occurred_at BETWEEN {0} AND {1}", start, end)
} else if (!start.isNullOrBlank()) {
wrapper.ge("occurred_at", start)
} else if (!end.isNullOrBlank()) {
wrapper.le("occurred_at", end)
}
if (!vendor.isNullOrBlank()) {
wrapper.eq("vendor", vendor)
}
if (itemId != null) {
wrapper.eq("related_item_id", itemId)
}
page(page, wrapper)
} }
if (!type.isNullOrBlank()) {
wrapper.eq(FinanceRecord::type, type)
}
if (!start.isNullOrBlank() && !end.isNullOrBlank()) {
wrapper.apply("occurred_at BETWEEN {0} AND {1}", start, end)
} else if (!start.isNullOrBlank()) {
wrapper.ge(FinanceRecord::occurredAt, start)
} else if (!end.isNullOrBlank()) {
wrapper.le(FinanceRecord::occurredAt, end)
}
if (!vendor.isNullOrBlank()) {
wrapper.eq(FinanceRecord::vendor, vendor)
}
if (itemId != null) {
wrapper.eq(FinanceRecord::relatedItemId, itemId)
}
page(page, wrapper)
} }
override fun listByType(type: String): Mono<List<FinanceRecord>> = override fun listByType(type: String): Mono<List<FinanceRecord>> =
blockingCall { blockingCall {
logger.info("Searching finance records by type") logger.info("Searching finance records by type")
ktQuery().eq(FinanceRecord::type, type).list() ktQuery()
.eq(FinanceRecord::type, type)
.list()
} }
override fun listByDateRange(start: String, end: String): Mono<List<FinanceRecord>> = override fun listByDateRange(start: String, end: String): Mono<List<FinanceRecord>> =
blockingCall { blockingCall {
logger.info("Searching finance records by date range") logger.info("Searching finance records by date range")
ktQuery().apply("occurred_at BETWEEN {0} AND {1}", start, end).list() ktQuery()
.apply("occurred_at BETWEEN {0} AND {1}", start, end)
.list()
} }
override fun listByVendor(vendor: String): Mono<List<FinanceRecord>> = override fun listByVendor(vendor: String): Mono<List<FinanceRecord>> =
blockingCall { blockingCall {
logger.info("Searching finance records by vendor") logger.info("Searching finance records by vendor")
ktQuery().eq(FinanceRecord::vendor, vendor).list() ktQuery()
.eq(FinanceRecord::vendor, vendor)
.list()
} }
override fun listByRelatedItem(itemId: Long): Mono<List<FinanceRecord>> = override fun listByRelatedItem(itemId: Long): Mono<List<FinanceRecord>> =
blockingCall { blockingCall {
logger.info("Searching finance records by related item id") logger.info("Searching finance records by related item id")
ktQuery().eq(FinanceRecord::relatedItemId, itemId).list() ktQuery()
.eq(FinanceRecord::relatedItemId, itemId)
.list()
} }
override fun createFinanceRecord(record: FinanceRecord): Mono<Boolean> = override fun createFinanceRecord(record: FinanceRecord): Mono<Boolean> =
@@ -3,11 +3,12 @@ package club.itstudio.itmanager.service.impl
import club.itstudio.itmanager.entity.dto.Item import club.itstudio.itmanager.entity.dto.Item
import club.itstudio.itmanager.mapper.ItemMapper import club.itstudio.itmanager.mapper.ItemMapper
import club.itstudio.itmanager.service.ItemService import club.itstudio.itmanager.service.ItemService
import com.baomidou.mybatisplus.extension.kotlin.KtQueryWrapper import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper
import com.baomidou.mybatisplus.extension.plugins.pagination.Page import com.baomidou.mybatisplus.extension.plugins.pagination.Page
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers import reactor.core.scheduler.Schedulers
@@ -22,40 +23,54 @@ class ItemServiceImpl : ServiceImpl<ItemMapper, Item>(), ItemService {
override fun pageItems(page: Page<Item>, keyword: String?, status: String?): Mono<Page<Item>> = override fun pageItems(page: Page<Item>, keyword: String?, status: String?): Mono<Page<Item>> =
blockingCall { blockingCall {
logger.info("Paging items with keyword and status") logger.info("Paging items with keyword and status")
val wrapper = KtQueryWrapper(Item()) if (keyword.isNullOrBlank() && status.isNullOrBlank()) {
if (!keyword.isNullOrBlank()) { page(page)
wrapper.and { } else {
it.like(Item::name, keyword).or().like(Item::code, keyword) val wrapper = QueryWrapper<Item>()
if (!keyword.isNullOrBlank()) {
wrapper.and {
it.like("name", keyword)
.or().like("code", keyword)
}
} }
if (!status.isNullOrBlank()) {
wrapper.eq("status", status)
}
page(page, wrapper)
} }
if (!status.isNullOrBlank()) {
wrapper.eq(Item::status, status)
}
page(page, wrapper)
} }
override fun getItemById(id: Long): Mono<Item> = override fun getItemById(id: Long): Mono<Item> =
blockingCall { blockingCall {
logger.info("Searching item by id") logger.info("Searching item by id")
ktQuery().eq(Item::id, id).one() ktQuery()
.eq(Item::id, id)
.one()
}.switchIfEmpty(Mono.error(NoSuchElementException("Item not found"))) }.switchIfEmpty(Mono.error(NoSuchElementException("Item not found")))
override fun listAvailableItems(): Mono<List<Item>> = override fun listAvailableItems(): Mono<List<Item>> =
blockingCall { blockingCall {
logger.info("Listing available items") logger.info("Listing available items")
ktQuery().gt(Item::availableQuantity, 0).eq(Item::status, "normal").list() ktQuery()
.gt(Item::availableQuantity, 0)
.eq(Item::status, "normal")
.list()
} }
override fun listInUseItems(): Mono<List<Item>> = override fun listInUseItems(): Mono<List<Item>> =
blockingCall { blockingCall {
logger.info("Listing in-use items") logger.info("Listing in-use items")
ktQuery().apply("total_quantity - available_quantity > 0").list() ktQuery()
.apply("total_quantity - available_quantity > 0")
.list()
} }
override fun listItemsByCategory(categoryId: Long): Mono<List<Item>> = override fun listItemsByCategory(categoryId: Long): Mono<List<Item>> =
blockingCall { blockingCall {
logger.info("Listing items by category") logger.info("Listing items by category")
ktQuery().eq(Item::categoryId, categoryId).list() ktQuery()
.eq(Item::categoryId, categoryId)
.list()
} }
override fun createItem(item: Item): Mono<Boolean> = override fun createItem(item: Item): Mono<Boolean> =
@@ -79,35 +94,42 @@ class ItemServiceImpl : ServiceImpl<ItemMapper, Item>(), ItemService {
override fun setPrimaryImage(id: Long, imagePath: String): Mono<Boolean> = override fun setPrimaryImage(id: Long, imagePath: String): Mono<Boolean> =
blockingCall { blockingCall {
logger.info("Setting primary image for item") logger.info("Setting primary image for item")
ktUpdate().eq(Item::id, id).set(Item::imagePath, imagePath).update() ktUpdate()
.eq(Item::id, id)
.set(Item::imagePath, imagePath)
.update()
} }
override fun borrowItem(id: Long, quantity: Int): Mono<Boolean> = override fun borrowItem(id: Long, quantity: Int): Mono<Boolean> =
blockingCall { blockingCall {
val item = ktQuery().eq(Item::id, id).one() val item = ktQuery().eq(Item::id, id).one()
if (item != null && item.availableQuantity >= quantity) { item != null
ktUpdate().eq(Item::id, id).set(Item::availableQuantity, item.availableQuantity - quantity).update() && item.availableQuantity >= quantity
} else { && ktUpdate()
false .eq(Item::id, id)
} .set(Item::availableQuantity, item.availableQuantity - quantity)
.update()
} }
override fun returnItem(id: Long, quantity: Int): Mono<Boolean> = override fun returnItem(id: Long, quantity: Int): Mono<Boolean> =
blockingCall { blockingCall {
val item = ktQuery().eq(Item::id, id).one() val item = ktQuery().eq(Item::id, id).one()
if (item != null && item.totalQuantity >= item.availableQuantity + quantity) { item != null
ktUpdate().eq(Item::id, id).set(Item::availableQuantity, item.availableQuantity + quantity).update() && item.totalQuantity >= item.availableQuantity + quantity
} else { && ktUpdate()
false .eq(Item::id, id)
} .set(Item::availableQuantity, item.availableQuantity + quantity)
.update()
} }
@Transactional
override fun batchInsertItems(items: List<Item>): Mono<Boolean> = override fun batchInsertItems(items: List<Item>): Mono<Boolean> =
blockingCall { blockingCall {
logger.info("Batch inserting items") logger.info("Batch inserting items")
saveBatch(items) saveBatch(items)
} }
@Transactional
override fun replaceAllItems(items: List<Item>): Mono<Boolean> = override fun replaceAllItems(items: List<Item>): Mono<Boolean> =
blockingCall { blockingCall {
logger.info("Replacing all items") logger.info("Replacing all items")
@@ -8,7 +8,6 @@ import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers import reactor.core.scheduler.Schedulers
import java.util.NoSuchElementException
@Service @Service
class ItemTransactionServiceImpl : ServiceImpl<ItemTransactionMapper, ItemTransaction>(), ItemTransactionService { class ItemTransactionServiceImpl : ServiceImpl<ItemTransactionMapper, ItemTransaction>(), ItemTransactionService {
@@ -21,25 +20,33 @@ class ItemTransactionServiceImpl : ServiceImpl<ItemTransactionMapper, ItemTransa
override fun getTransactionById(id: Long): Mono<ItemTransaction> = override fun getTransactionById(id: Long): Mono<ItemTransaction> =
blockingCall { blockingCall {
logger.info("Searching transaction by id") logger.info("Searching transaction by id")
ktQuery().eq(ItemTransaction::id, id).one() ktQuery()
.eq(ItemTransaction::id, id)
.one()
}.switchIfEmpty(Mono.error(NoSuchElementException("ItemTransaction not found"))) }.switchIfEmpty(Mono.error(NoSuchElementException("ItemTransaction not found")))
override fun listByItem(itemId: Long): Mono<List<ItemTransaction>> = override fun listByItem(itemId: Long): Mono<List<ItemTransaction>> =
blockingCall { blockingCall {
logger.info("Searching transactions by item id") logger.info("Searching transactions by item id")
ktQuery().eq(ItemTransaction::itemId, itemId).list() ktQuery()
.eq(ItemTransaction::itemId, itemId)
.list()
} }
override fun listByType(transType: String): Mono<List<ItemTransaction>> = override fun listByType(transType: String): Mono<List<ItemTransaction>> =
blockingCall { blockingCall {
logger.info("Searching transactions by type") logger.info("Searching transactions by type")
ktQuery().eq(ItemTransaction::transType, transType).list() ktQuery()
.eq(ItemTransaction::transType, transType)
.list()
} }
override fun listByOperator(operatorId: Long): Mono<List<ItemTransaction>> = override fun listByOperator(operatorId: Long): Mono<List<ItemTransaction>> =
blockingCall { blockingCall {
logger.info("Searching transactions by operator id") logger.info("Searching transactions by operator id")
ktQuery().eq(ItemTransaction::operatorId, operatorId).list() ktQuery()
.eq(ItemTransaction::operatorId, operatorId)
.list()
} }
override fun createTransaction(transaction: ItemTransaction): Mono<Boolean> = override fun createTransaction(transaction: ItemTransaction): Mono<Boolean> =
@@ -9,22 +9,18 @@ import club.itstudio.itmanager.service.AccountService
import club.itstudio.itmanager.service.AttachmentService import club.itstudio.itmanager.service.AttachmentService
import club.itstudio.itmanager.service.CategoryService import club.itstudio.itmanager.service.CategoryService
import club.itstudio.itmanager.service.FinanceRecordService import club.itstudio.itmanager.service.FinanceRecordService
import club.itstudio.itmanager.service.FileStorageService
import club.itstudio.itmanager.utils.Const import club.itstudio.itmanager.utils.Const
import club.itstudio.itmanager.utils.DateUtils.getCurrentDateTime import club.itstudio.itmanager.utils.DateUtils.getCurrentDateTime
import com.baomidou.mybatisplus.extension.plugins.pagination.Page import com.baomidou.mybatisplus.extension.plugins.pagination.Page
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.amqp.core.AmqpTemplate import org.springframework.amqp.core.AmqpTemplate
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.codec.multipart.FilePart import org.springframework.http.codec.multipart.FilePart
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers import reactor.core.scheduler.Schedulers
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Paths
import java.util.NoSuchElementException import java.util.NoSuchElementException
import java.util.UUID
@Service @Service
class FinanceApiService( class FinanceApiService(
@@ -33,9 +29,7 @@ class FinanceApiService(
private val attachmentService: AttachmentService, private val attachmentService: AttachmentService,
private val accountService: AccountService, private val accountService: AccountService,
private val amqpTemplate: AmqpTemplate, private val amqpTemplate: AmqpTemplate,
private val fileStorageService: FileStorageService
@Value($$"${app.file.data-path}")
private val dataPath: String
) { ) {
private val logger = LoggerFactory.getLogger(this::class.java) private val logger = LoggerFactory.getLogger(this::class.java)
@@ -171,41 +165,25 @@ class FinanceApiService(
} }
private fun saveFinanceImage(financeRecordId: Long, file: FilePart): Mono<Attachment> { private fun saveFinanceImage(financeRecordId: Long, file: FilePart): Mono<Attachment> {
val directory = Paths.get(dataPath, Const.FILE_DIR_IMAGE, "finance", financeRecordId.toString()) return fileStorageService.saveImage(file, "finance", financeRecordId)
val savedName = generateSafeFileName(file.filename()) .map { stored ->
val target = directory.resolve(savedName) Attachment(
return Mono.fromCallable { relatedTable = "finance_records",
Files.createDirectories(directory) relatedId = financeRecordId,
file.transferTo(target) fileName = stored.path.fileName.toString(),
Attachment( filePath = stored.path.toString(),
relatedTable = "finance_records", fileSize = stored.size,
relatedId = financeRecordId, contentType = file.headers().contentType?.toString(),
fileName = savedName, uploadedAt = getCurrentDateTime()
filePath = target.toString(), )
fileSize = Files.size(target),
contentType = file.headers().contentType?.toString(),
uploadedAt = getCurrentDateTime()
)
}.subscribeOn(Schedulers.boundedElastic())
}
private fun generateSafeFileName(original: String?): String {
val extension = original
?.substringAfterLast('.')
?.lowercase()
?.takeIf {
it.isNotBlank() &&
it.length <= 10 &&
it.all { ch -> ch.isLetterOrDigit() }
} }
return if (extension != null) "${UUID.randomUUID()}.$extension" else UUID.randomUUID().toString()
} }
private fun cleanupFinanceAttachments(financeRecordId: Long): Mono<Boolean> { private fun cleanupFinanceAttachments(financeRecordId: Long): Mono<Boolean> {
return attachmentService.listByRelated("finance_records", financeRecordId) return attachmentService.listByRelated("finance_records", financeRecordId)
.flatMap { attachments -> .flatMap { attachments ->
Flux.fromIterable(attachments) Flux.fromIterable(attachments)
.flatMap { deletePhysicalFile(it.filePath) } .flatMap { fileStorageService.deleteFile(it.filePath) }
.then(attachmentService.deleteByRelated("finance_records", financeRecordId)) .then(attachmentService.deleteByRelated("finance_records", financeRecordId))
} }
.onErrorResume { error -> .onErrorResume { error ->
@@ -236,15 +214,4 @@ class FinanceApiService(
} }
} }
} }
private fun deletePhysicalFile(path: String?): Mono<Void> {
if (path.isNullOrBlank()) return Mono.empty()
return Mono.fromRunnable<Void> {
try {
Files.deleteIfExists(Paths.get(path))
} catch (e: IOException) {
logger.warn("Failed to delete file: {}", path, e)
}
}.subscribeOn(Schedulers.boundedElastic())
}
} }
@@ -7,23 +7,18 @@ import club.itstudio.itmanager.entity.dto.ItemTransaction
import club.itstudio.itmanager.entity.enums.TransactionType import club.itstudio.itmanager.entity.enums.TransactionType
import club.itstudio.itmanager.service.AttachmentService import club.itstudio.itmanager.service.AttachmentService
import club.itstudio.itmanager.service.CategoryService import club.itstudio.itmanager.service.CategoryService
import club.itstudio.itmanager.service.FileStorageService
import club.itstudio.itmanager.service.ItemService import club.itstudio.itmanager.service.ItemService
import club.itstudio.itmanager.service.ItemTransactionService import club.itstudio.itmanager.service.ItemTransactionService
import club.itstudio.itmanager.utils.Const import club.itstudio.itmanager.utils.Const
import club.itstudio.itmanager.utils.DateUtils.getCurrentDateTime import club.itstudio.itmanager.utils.DateUtils.getCurrentDateTime
import com.baomidou.mybatisplus.extension.plugins.pagination.Page import com.baomidou.mybatisplus.extension.plugins.pagination.Page
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.codec.multipart.FilePart import org.springframework.http.codec.multipart.FilePart
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Paths
import java.util.NoSuchElementException import java.util.NoSuchElementException
import java.util.UUID
@Service @Service
class ItemsApiService( class ItemsApiService(
@@ -31,9 +26,7 @@ class ItemsApiService(
private val categoryService: CategoryService, private val categoryService: CategoryService,
private val attachmentService: AttachmentService, private val attachmentService: AttachmentService,
private val transactionService: ItemTransactionService, private val transactionService: ItemTransactionService,
private val fileStorageService: FileStorageService
@Value($$"${app.file.data-path}")
private val dataPath: String
) { ) {
private val logger = LoggerFactory.getLogger(this::class.java) private val logger = LoggerFactory.getLogger(this::class.java)
@@ -276,7 +269,7 @@ class ItemsApiService(
.flatMap { attachment -> .flatMap { attachment ->
attachmentService.deleteAttachment(id).flatMap { success -> attachmentService.deleteAttachment(id).flatMap { success ->
if (success) { if (success) {
deletePhysicalFile(attachment.filePath).thenReturn("") fileStorageService.deleteFile(attachment.filePath).thenReturn("")
} else { } else {
Mono.just("item image delete failed.") Mono.just("item image delete failed.")
} }
@@ -296,41 +289,25 @@ class ItemsApiService(
} }
private fun saveItemImage(itemId: Long, file: FilePart): Mono<Attachment> { private fun saveItemImage(itemId: Long, file: FilePart): Mono<Attachment> {
val directory = Paths.get(dataPath, Const.FILE_DIR_IMAGE, "items", itemId.toString()) return fileStorageService.saveImage(file, "items", itemId)
val savedName = generateSafeFileName(file.filename()) .map { stored ->
val target = directory.resolve(savedName) Attachment(
return Mono.fromCallable { relatedTable = "items",
Files.createDirectories(directory) relatedId = itemId,
file.transferTo(target) fileName = stored.path.fileName.toString(),
Attachment( filePath = stored.path.toString(),
relatedTable = "items", fileSize = stored.size,
relatedId = itemId, contentType = file.headers().contentType?.toString(),
fileName = savedName, uploadedAt = getCurrentDateTime()
filePath = target.toString(), )
fileSize = Files.size(target),
contentType = file.headers().contentType?.toString(),
uploadedAt = getCurrentDateTime()
)
}.subscribeOn(Schedulers.boundedElastic())
}
private fun generateSafeFileName(original: String?): String {
val extension = original
?.substringAfterLast('.')
?.lowercase()
?.takeIf {
it.isNotBlank() &&
it.length <= 10 &&
it.all { ch -> ch.isLetterOrDigit() }
} }
return if (extension != null) "${UUID.randomUUID()}.$extension" else UUID.randomUUID().toString()
} }
private fun cleanupItemAttachments(itemId: Long): Mono<String> { private fun cleanupItemAttachments(itemId: Long): Mono<String> {
return attachmentService.listByRelated("items", itemId) return attachmentService.listByRelated("items", itemId)
.flatMap { attachments -> .flatMap { attachments ->
Flux.fromIterable(attachments) Flux.fromIterable(attachments)
.flatMap { deletePhysicalFile(it.filePath) } .flatMap { fileStorageService.deleteFile(it.filePath) }
.then(attachmentService.deleteByRelated("items", itemId)) .then(attachmentService.deleteByRelated("items", itemId))
.thenReturn("") .thenReturn("")
} }
@@ -339,15 +316,4 @@ class ItemsApiService(
Mono.just("") Mono.just("")
} }
} }
private fun deletePhysicalFile(path: String?): Mono<Void> {
if (path.isNullOrBlank()) return Mono.empty()
return Mono.fromRunnable<Void> {
try {
Files.deleteIfExists(Paths.get(path))
} catch (e: IOException) {
logger.warn("Failed to delete file: {}", path, e)
}
}.subscribeOn(Schedulers.boundedElastic())
}
} }
@@ -4,29 +4,22 @@ import club.itstudio.itmanager.entity.dto.Attachment
import club.itstudio.itmanager.entity.dto.BorrowRecord import club.itstudio.itmanager.entity.dto.BorrowRecord
import club.itstudio.itmanager.service.AttachmentService import club.itstudio.itmanager.service.AttachmentService
import club.itstudio.itmanager.service.BorrowRecordService import club.itstudio.itmanager.service.BorrowRecordService
import club.itstudio.itmanager.service.FileStorageService
import club.itstudio.itmanager.utils.Const import club.itstudio.itmanager.utils.Const
import club.itstudio.itmanager.utils.DateUtils.getCurrentDateTime import club.itstudio.itmanager.utils.DateUtils.getCurrentDateTime
import com.baomidou.mybatisplus.extension.plugins.pagination.Page import com.baomidou.mybatisplus.extension.plugins.pagination.Page
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.codec.multipart.FilePart import org.springframework.http.codec.multipart.FilePart
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Flux import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Paths
import java.util.NoSuchElementException import java.util.NoSuchElementException
import java.util.UUID
@Service @Service
class UsagesApiService( class UsagesApiService(
private val borrowRecordService: BorrowRecordService, private val borrowRecordService: BorrowRecordService,
private val attachmentService: AttachmentService, private val attachmentService: AttachmentService,
private val fileStorageService: FileStorageService
@Value($$"${app.file.data-path}")
private val dataPath: String
) { ) {
private val logger = LoggerFactory.getLogger(this::class.java) private val logger = LoggerFactory.getLogger(this::class.java)
@@ -149,7 +142,7 @@ class UsagesApiService(
.flatMap { attachment -> .flatMap { attachment ->
attachmentService.deleteAttachment(id).flatMap { success -> attachmentService.deleteAttachment(id).flatMap { success ->
if (success) { if (success) {
deletePhysicalFile(attachment.filePath).thenReturn("") fileStorageService.deleteFile(attachment.filePath).thenReturn("")
} else { } else {
Mono.just("usage image delete failed.") Mono.just("usage image delete failed.")
} }
@@ -160,41 +153,25 @@ class UsagesApiService(
} }
private fun saveUsageImage(usageId: Long, file: FilePart): Mono<Attachment> { private fun saveUsageImage(usageId: Long, file: FilePart): Mono<Attachment> {
val directory = Paths.get(dataPath, Const.FILE_DIR_IMAGE, "borrow_records", usageId.toString()) return fileStorageService.saveImage(file, "borrow_records", usageId)
val savedName = generateSafeFileName(file.filename()) .map { stored ->
val target = directory.resolve(savedName) Attachment(
return Mono.fromCallable { relatedTable = RELATED_TABLE,
Files.createDirectories(directory) relatedId = usageId,
file.transferTo(target) fileName = stored.path.fileName.toString(),
Attachment( filePath = stored.path.toString(),
relatedTable = RELATED_TABLE, fileSize = stored.size,
relatedId = usageId, contentType = file.headers().contentType?.toString(),
fileName = savedName, uploadedAt = getCurrentDateTime()
filePath = target.toString(), )
fileSize = Files.size(target),
contentType = file.headers().contentType?.toString(),
uploadedAt = getCurrentDateTime()
)
}.subscribeOn(Schedulers.boundedElastic())
}
private fun generateSafeFileName(original: String?): String {
val extension = original
?.substringAfterLast('.')
?.lowercase()
?.takeIf {
it.isNotBlank() &&
it.length <= 10 &&
it.all { ch -> ch.isLetterOrDigit() }
} }
return if (extension != null) "${UUID.randomUUID()}.$extension" else UUID.randomUUID().toString()
} }
private fun cleanupUsageAttachments(usageId: Long): Mono<String> { private fun cleanupUsageAttachments(usageId: Long): Mono<String> {
return attachmentService.listByRelated(RELATED_TABLE, usageId) return attachmentService.listByRelated(RELATED_TABLE, usageId)
.flatMap { attachments -> .flatMap { attachments ->
Flux.fromIterable(attachments) Flux.fromIterable(attachments)
.flatMap { deletePhysicalFile(it.filePath) } .flatMap { fileStorageService.deleteFile(it.filePath) }
.then(attachmentService.deleteByRelated(RELATED_TABLE, usageId)) .then(attachmentService.deleteByRelated(RELATED_TABLE, usageId))
.thenReturn("") .thenReturn("")
} }
@@ -204,17 +181,6 @@ class UsagesApiService(
} }
} }
private fun deletePhysicalFile(path: String?): Mono<Void> {
if (path.isNullOrBlank()) return Mono.empty()
return Mono.fromRunnable<Void> {
try {
Files.deleteIfExists(Paths.get(path))
} catch (e: IOException) {
logger.warn("Failed to delete file: {}", path, e)
}
}.subscribeOn(Schedulers.boundedElastic())
}
companion object { companion object {
private const val RELATED_TABLE = "borrow_records" private const val RELATED_TABLE = "borrow_records"
private const val STATUS_BORROWED = "BORROWED" private const val STATUS_BORROWED = "BORROWED"
@@ -9,8 +9,6 @@ import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.contextual
import java.math.BigDecimal import java.math.BigDecimal
object BigDecimalSerializer : KSerializer<BigDecimal> { object BigDecimalSerializer : KSerializer<BigDecimal> {
@@ -29,7 +27,3 @@ object BigDecimalSerializer : KSerializer<BigDecimal> {
return element.content.toBigDecimal() return element.content.toBigDecimal()
} }
} }
val customSerializersModule = SerializersModule {
contextual(BigDecimalSerializer)
}