Compare commits

...

9 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
server_main a1bcde0a37 fix: mybatis plus and kotlinx LocalDateTime 2026-08-18 06:41:50 +00:00
SOR2171 bb3df45b9f Merge pull request 'refactor: wrong position of query functions' (#8) from backend-dev into main
Reviewed-on: #8
2026-08-18 14:08:21 +08:00
SOR 92ffc47169 refactor: wrong position of query functions 2026-08-18 14:06:49 +08:00
25 changed files with 419 additions and 275 deletions
+3
View File
@@ -13,5 +13,8 @@
</list> </list>
</option> </option>
</inspection_tool> </inspection_tool>
<inspection_tool class="ShellCheck" enabled="true" level="ERROR" enabled_by_default="true">
<shellcheck_settings value="SC2317" />
</inspection_tool>
</profile> </profile>
</component> </component>
+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,49 @@
package club.itstudio.itmanager.mapper.typehandler
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.toJavaLocalDateTime
import kotlinx.datetime.toKotlinLocalDateTime
import org.apache.ibatis.type.BaseTypeHandler
import org.apache.ibatis.type.JdbcType
import org.apache.ibatis.type.MappedJdbcTypes
import org.apache.ibatis.type.MappedTypes
import java.sql.CallableStatement
import java.sql.PreparedStatement
import java.sql.ResultSet
@MappedTypes(LocalDateTime::class)
@MappedJdbcTypes(JdbcType.TIMESTAMP)
class TimeTypeHandler : BaseTypeHandler<LocalDateTime>() {
override fun setNonNullParameter(
ps: PreparedStatement,
i: Int,
parameter: LocalDateTime,
jdbcType: JdbcType?
) {
ps.setObject(i, parameter.toJavaLocalDateTime())
}
override fun getNullableResult(
rs: ResultSet,
columnName: String
): LocalDateTime? =
rs.getTimestamp(columnName)
?.toLocalDateTime()
?.toKotlinLocalDateTime()
override fun getNullableResult(
rs: ResultSet,
columnIndex: Int
): LocalDateTime? =
rs.getTimestamp(columnIndex)
?.toLocalDateTime()
?.toKotlinLocalDateTime()
override fun getNullableResult(
cs: CallableStatement,
columnIndex: Int
): LocalDateTime? =
cs.getTimestamp(columnIndex)
?.toLocalDateTime()
?.toKotlinLocalDateTime()
}
@@ -1,20 +1,19 @@
package club.itstudio.itmanager.service package club.itstudio.itmanager.service
import com.baomidou.mybatisplus.extension.service.IService
import club.itstudio.itmanager.entity.dto.Account import club.itstudio.itmanager.entity.dto.Account
import club.itstudio.itmanager.entity.vo.request.EmailRegisterVO import com.baomidou.mybatisplus.extension.service.IService
import club.itstudio.itmanager.entity.vo.request.PasswordResetVO
import club.itstudio.itmanager.entity.vo.response.ReloginVO
import org.springframework.security.core.userdetails.ReactiveUserDetailsService import org.springframework.security.core.userdetails.ReactiveUserDetailsService
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
interface AccountService : IService<Account>, ReactiveUserDetailsService { interface AccountService : IService<Account>, ReactiveUserDetailsService {
fun findAccountById(id: Long): Mono<Account>
fun findAccountByNameOrEmail(text: String): Mono<Account> fun findAccountByNameOrEmail(text: String): Mono<Account>
fun findAccountByName(username: String): Mono<Account> fun findAccountByName(username: String): Mono<Account>
fun findAccountByEmail(email: String): Mono<Account> fun findAccountByEmail(email: String): Mono<Account>
fun existAccountByName(username: String): Mono<Boolean> fun existAccountByName(username: String): Mono<Boolean>
fun existAccountByEmail(email: String): Mono<Boolean> fun existAccountByEmail(email: String): Mono<Boolean>
fun resetPasswordByEmail(email: String, encodedPassword: String): Mono<Void> fun resetPasswordByEmail(email: String, encodedPassword: String): Mono<Void>
fun createAccount(account: Account): Mono<Boolean>
// fun batchInsert(accounts: List<Account>): Mono<Boolean> // fun batchInsert(accounts: List<Account>): Mono<Boolean>
// fun replaceAll(accounts: List<Account>): Mono<Boolean> // fun replaceAll(accounts: List<Account>): Mono<Boolean>
@@ -9,6 +9,7 @@ interface AttachmentService : IService<Attachment> {
fun listByRelated(relatedTable: String, relatedId: Long): Mono<List<Attachment>> fun listByRelated(relatedTable: String, relatedId: Long): Mono<List<Attachment>>
fun listByUploader(uploadedBy: Long): Mono<List<Attachment>> fun listByUploader(uploadedBy: Long): Mono<List<Attachment>>
fun createAttachment(attachment: Attachment): Mono<Boolean> fun createAttachment(attachment: Attachment): Mono<Boolean>
fun updateAttachment(attachment: Attachment): Mono<Boolean>
fun deleteAttachment(id: Long): Mono<Boolean> fun deleteAttachment(id: Long): Mono<Boolean>
fun deleteByRelated(relatedTable: String, relatedId: Long): Mono<Boolean> fun deleteByRelated(relatedTable: String, relatedId: Long): Mono<Boolean>
fun exportAllAttachments(): Mono<List<Attachment>> fun exportAllAttachments(): Mono<List<Attachment>>
@@ -2,10 +2,12 @@ package club.itstudio.itmanager.service
import com.baomidou.mybatisplus.extension.service.IService import com.baomidou.mybatisplus.extension.service.IService
import club.itstudio.itmanager.entity.dto.BorrowRecord import club.itstudio.itmanager.entity.dto.BorrowRecord
import com.baomidou.mybatisplus.extension.plugins.pagination.Page
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
interface BorrowRecordService : IService<BorrowRecord> { interface BorrowRecordService : IService<BorrowRecord> {
fun getBorrowRecordById(id: Long): Mono<BorrowRecord> fun getBorrowRecordById(id: Long): Mono<BorrowRecord>
fun pageUsages(page: Page<BorrowRecord>, keyword: String?, status: String?): Mono<Page<BorrowRecord>>
fun listByItem(itemId: Long): Mono<List<BorrowRecord>> fun listByItem(itemId: Long): Mono<List<BorrowRecord>>
fun listByBorrower(borrower: String): Mono<List<BorrowRecord>> fun listByBorrower(borrower: String): Mono<List<BorrowRecord>>
fun listByStatus(status: String): Mono<List<BorrowRecord>> fun listByStatus(status: String): Mono<List<BorrowRecord>>
@@ -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()
}
}
@@ -2,10 +2,20 @@ package club.itstudio.itmanager.service
import com.baomidou.mybatisplus.extension.service.IService import com.baomidou.mybatisplus.extension.service.IService
import club.itstudio.itmanager.entity.dto.FinanceRecord import club.itstudio.itmanager.entity.dto.FinanceRecord
import com.baomidou.mybatisplus.extension.plugins.pagination.Page
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
interface FinanceRecordService : IService<FinanceRecord> { interface FinanceRecordService : IService<FinanceRecord> {
fun getFinanceRecordById(id: Long): Mono<FinanceRecord> fun getFinanceRecordById(id: Long): Mono<FinanceRecord>
fun pageFinanceRecords(
page: Page<FinanceRecord>,
keyword: String?,
type: String?,
start: String?,
end: String?,
vendor: String?,
itemId: Long?
): Mono<Page<FinanceRecord>>
fun listByType(type: String): Mono<List<FinanceRecord>> fun listByType(type: String): Mono<List<FinanceRecord>>
fun listByDateRange(start: String, end: String): Mono<List<FinanceRecord>> fun listByDateRange(start: String, end: String): Mono<List<FinanceRecord>>
fun listByVendor(vendor: String): Mono<List<FinanceRecord>> fun listByVendor(vendor: String): Mono<List<FinanceRecord>>
@@ -47,6 +47,14 @@ class AccountServiceImpl : ServiceImpl<AccountMapper, Account>(), AccountService
.one() .one()
} }
override fun findAccountById(id: Long): Mono<Account> =
blockingCall {
logger.info("Searching account by id")
ktQuery()
.eq(Account::id, id)
.one()
}
override fun findAccountByEmail(email: String): Mono<Account> = override fun findAccountByEmail(email: String): Mono<Account> =
findAccount("email") { findAccount("email") {
ktQuery() ktQuery()
@@ -141,4 +149,10 @@ class AccountServiceImpl : ServiceImpl<AccountMapper, Account>(), AccountService
} }
} }
} }
override fun createAccount(account: Account): Mono<Boolean> =
blockingCall {
logger.info("Creating account")
save(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> =
@@ -42,6 +48,12 @@ class AttachmentServiceImpl : ServiceImpl<AttachmentMapper, Attachment>(), Attac
save(attachment) save(attachment)
} }
override fun updateAttachment(attachment: Attachment): Mono<Boolean> =
blockingCall {
logger.info("Updating attachment")
updateById(attachment)
}
override fun deleteAttachment(id: Long): Mono<Boolean> = override fun deleteAttachment(id: Long): Mono<Boolean> =
blockingCall { blockingCall {
logger.info("Deleting attachment by id") logger.info("Deleting attachment by id")
@@ -51,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,12 +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.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 {
@@ -21,31 +23,64 @@ 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(
page: Page<BorrowRecord>,
keyword: String?,
status: String?
): Mono<Page<BorrowRecord>> =
blockingCall {
logger.info("Paging borrow records")
if (keyword.isNullOrBlank() && status.isNullOrBlank()) {
page.addOrder(OrderItem.desc("created_at"))
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)
}
}
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,12 +3,13 @@ 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.core.conditions.query.QueryWrapper
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 FinanceRecordServiceImpl : ServiceImpl<FinanceRecordMapper, FinanceRecord>(), FinanceRecordService { class FinanceRecordServiceImpl : ServiceImpl<FinanceRecordMapper, FinanceRecord>(), FinanceRecordService {
@@ -21,31 +22,85 @@ 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(
page: Page<FinanceRecord>,
keyword: String?,
type: String?,
start: String?,
end: String?,
vendor: String?,
itemId: Long?
): Mono<Page<FinanceRecord>> =
blockingCall {
logger.info("Paging finance records")
if (keyword.isNullOrBlank() && type.isNullOrBlank() && start.isNullOrBlank() &&
end.isNullOrBlank() && vendor.isNullOrBlank() && itemId == null
) {
page(page)
} else {
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)
}
}
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,13 +3,14 @@ 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.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
import java.util.NoSuchElementException
@Service @Service
class ItemServiceImpl : ServiceImpl<ItemMapper, Item>(), ItemService { class ItemServiceImpl : ServiceImpl<ItemMapper, Item>(), ItemService {
@@ -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 = ktQuery() 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> =
@@ -77,7 +77,7 @@ class AuthorizationService(
isStaff = false, isStaff = false,
createdAt = getCurrentDateTime() createdAt = getCurrentDateTime()
) )
Mono.fromCallable { accountService.save(account) } accountService.createAccount(account)
.flatMap { success -> .flatMap { success ->
if (success) { if (success) {
stringRedisTemplate.delete(Const.VERIFY_EMAIL_DATA + email) stringRedisTemplate.delete(Const.VERIFY_EMAIL_DATA + email)
@@ -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)
@@ -49,46 +43,9 @@ class FinanceApiService(
itemId: Long? itemId: Long?
): Mono<Page<FinanceRecord>> { ): Mono<Page<FinanceRecord>> {
logger.info("Listing finance records") logger.info("Listing finance records")
return pageFinanceRecords(page, keyword, type, start, end, vendor, itemId) return financeRecordService.pageFinanceRecords(page, keyword, type, start, end, vendor, itemId)
} }
private fun pageFinanceRecords(
page: Page<FinanceRecord>,
keyword: String?,
type: String?,
start: String?,
end: String?,
vendor: String?,
itemId: Long?
): Mono<Page<FinanceRecord>> =
Mono.fromCallable {
val wrapper = financeRecordService.ktQuery()
if (!keyword.isNullOrBlank()) {
wrapper.and {
it.like(FinanceRecord::recordNo, keyword)
.or().like(FinanceRecord::vendor, keyword)
.or().like(FinanceRecord::note, keyword)
}
}
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)
}
financeRecordService.page(page, wrapper)
}.subscribeOn(Schedulers.boundedElastic())
fun createFinanceRecord(record: FinanceRecord): Mono<String> { fun createFinanceRecord(record: FinanceRecord): Mono<String> {
logger.info("Creating finance record") logger.info("Creating finance record")
val rightNow = getCurrentDateTime() val rightNow = getCurrentDateTime()
@@ -208,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 ->
@@ -253,8 +194,7 @@ class FinanceApiService(
private fun sendFinanceDeleteNotification(record: FinanceRecord): Mono<Void> { private fun sendFinanceDeleteNotification(record: FinanceRecord): Mono<Void> {
val recordedBy = record.recordedBy ?: return Mono.empty() val recordedBy = record.recordedBy ?: return Mono.empty()
return Mono.fromCallable { accountService.getById(recordedBy) } return accountService.findAccountById(recordedBy)
.subscribeOn(Schedulers.boundedElastic())
.flatMap { account -> .flatMap { account ->
val email = account.email val email = account.email
if (email.isNullOrBlank()) { if (email.isNullOrBlank()) {
@@ -274,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)
@@ -262,8 +255,7 @@ class ItemsApiService(
fun updateItemImage(attachment: Attachment): Mono<String> { fun updateItemImage(attachment: Attachment): Mono<String> {
logger.info("Updating item image") logger.info("Updating item image")
if (attachment.id == null) return Mono.just("item image id is required.") if (attachment.id == null) return Mono.just("item image id is required.")
return Mono.fromCallable { attachmentService.updateById(attachment) } return attachmentService.updateAttachment(attachment)
.subscribeOn(Schedulers.boundedElastic())
.flatMap { success -> .flatMap { success ->
if (success) Mono.just("") if (success) Mono.just("")
else Mono.just("item image update failed.") else Mono.just("item image update failed.")
@@ -277,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.")
} }
@@ -297,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("")
} }
@@ -340,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())
}
} }
@@ -41,8 +41,7 @@ class ProofImagesApiService(
fun updateProofImage(attachment: Attachment): Mono<String> { fun updateProofImage(attachment: Attachment): Mono<String> {
logger.info("Updating proof image") logger.info("Updating proof image")
if (attachment.id == null) return Mono.just("proof image id is required.") if (attachment.id == null) return Mono.just("proof image id is required.")
return Mono.fromCallable { attachmentService.updateById(attachment) } return attachmentService.updateAttachment(attachment)
.subscribeOn(Schedulers.boundedElastic())
.flatMap { success -> .flatMap { success ->
if (success) Mono.just("") if (success) Mono.just("")
else Mono.just("proof image update failed.") else Mono.just("proof image update failed.")
@@ -4,46 +4,28 @@ 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.core.toolkit.Wrappers
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)
fun getUsages(page: Page<BorrowRecord>, keyword: String?, status: String?): Mono<Page<BorrowRecord>> { fun getUsages(page: Page<BorrowRecord>, keyword: String?, status: String?): Mono<Page<BorrowRecord>> {
logger.info("Paging usages with keyword and status") logger.info("Paging usages with keyword and status")
return Mono.fromCallable { return borrowRecordService.pageUsages(page, keyword, status)
val wrapper = Wrappers.lambdaQuery<BorrowRecord>()
if (!keyword.isNullOrBlank()) {
wrapper.like(BorrowRecord::borrower, keyword)
}
if (!status.isNullOrBlank()) {
wrapper.eq(BorrowRecord::status, status)
}
wrapper.orderByDesc(BorrowRecord::createdAt)
borrowRecordService.page(page, wrapper)
}.subscribeOn(Schedulers.boundedElastic())
} }
fun createUsage(record: BorrowRecord): Mono<String> { fun createUsage(record: BorrowRecord): Mono<String> {
@@ -145,8 +127,7 @@ class UsagesApiService(
fun updateUsageImage(attachment: Attachment): Mono<String> { fun updateUsageImage(attachment: Attachment): Mono<String> {
logger.info("Updating usage image") logger.info("Updating usage image")
if (attachment.id == null) return Mono.just("usage image id is required.") if (attachment.id == null) return Mono.just("usage image id is required.")
return Mono.fromCallable { attachmentService.updateById(attachment) } return attachmentService.updateAttachment(attachment)
.subscribeOn(Schedulers.boundedElastic())
.flatMap { success -> .flatMap { success ->
if (success) Mono.just("") if (success) Mono.just("")
else Mono.just("usage image update failed.") else Mono.just("usage image update failed.")
@@ -161,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.")
} }
@@ -172,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("")
} }
@@ -216,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> {
@@ -28,8 +26,4 @@ object BigDecimalSerializer : KSerializer<BigDecimal> {
} }
return element.content.toBigDecimal() return element.content.toBigDecimal()
} }
}
val customSerializersModule = SerializersModule {
contextual(BigDecimalSerializer)
} }
@@ -1,7 +1,7 @@
spring: spring:
profiles: profiles:
active: private active: private
webflux: webflux:
base-path: / base-path: /
@@ -37,6 +37,9 @@ spring:
#username: root #username: root
#password: 123456 #password: 123456
mybatis-plus:
type-handlers-package: club.itstudio.itmanager.mapper.typehandler
app: app:
flow-limit: flow-limit:
count: 1000 count: 1000