feat(admin-borrows): add search endpoint replacing getAll

Replace the simple \"get all borrows\" endpoint with a search-based
approach supporting queries by book name, author, username, and role.

- Change endpoint from GET /getall to GET /search?query=xxx
- Add search service implementation using QueryWrapper with LIKE
  predicates across Book, User, and BorrowRecord tables
- Use kotlinx-coroutines for parallel async data fetching per result
- Add kotlinx-coroutines-core and kotlinx-coroutines-reactor deps
This commit is contained in:
2026-05-23 13:06:47 +08:00
parent 32aed36ebf
commit f73e0e3cba
6 changed files with 146 additions and 147 deletions
+2
View File
@@ -22,6 +22,8 @@ repositories {
dependencies { dependencies {
implementation("org.springframework.boot:spring-boot-starter") implementation("org.springframework.boot:spring-boot-starter")
implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.10.2")
implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.aspectj:aspectjweaver") implementation("org.aspectj:aspectjweaver")
implementation("org.springframework.security:spring-security-crypto") implementation("org.springframework.security:spring-security-crypto")
@@ -28,7 +28,42 @@ class AdminBorrowController(
private val adminBorrowService: AdminBorrowService, private val adminBorrowService: AdminBorrowService,
private val ipExtractor: IpExtractor private val ipExtractor: IpExtractor
) { ) {
/*
* GET /api/admin/borrows/search?query=xxx
* 按书名、作者、用户名或角色搜索借阅记录
*/
@RequireRole("admin")
@GetMapping("/search")
fun searchBorrows(
@RequestAttribute(required = false) username: String?,
query: String,
request: HttpServletRequest
): Result<Any?> {
log.info("[AdminBorrow] search: user={}, query={}", username ?: "guest", query)
log.info("[AdminBorrow] user agent: {}, ip={}", request.getHeader("User-Agent"), ipExtractor.getRealIp(request))
return adminBorrowService.searchBorrows(query)
}
/*
* GET /api/admin/borrows/getone?id=xxx
* 查询单条借阅记录,含关联的图书和用户信息
*/
@RequireRole("admin")
@GetMapping("/getone")
fun getOneBorrow(
@RequestAttribute(required = false) username: String?,
id: Long,
request: HttpServletRequest
): Result<Any?> {
log.info("[AdminBorrow] getOne: user={}, borrow record id={}", username ?: "guest", id)
log.info("[AdminBorrow] user agent: {}, ip={}", request.getHeader("User-Agent"), ipExtractor.getRealIp(request))
return adminBorrowService.getOneBorrow(id)
}
/*
* GET /api/admin/borrows/getall
* 查询全部借阅记录(仅管理员)
*/
@RequireRole("admin") @RequireRole("admin")
@GetMapping("/getall") @GetMapping("/getall")
fun getAllBorrows( fun getAllBorrows(
@@ -1,65 +0,0 @@
package com.msksbr.bookmgr.controller
import com.msksbr.bookmgr.annotation.RequireRole
import com.msksbr.bookmgr.config.IpExtractor
import com.msksbr.bookmgr.script.log
import com.msksbr.bookmgr.service.DashBoardService
import com.msksbr.bookmgr.template.Result
import jakarta.servlet.http.HttpServletRequest
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestAttribute
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
/*
* 仪表盘接口
* 路径前缀:/api/dashboard
*
* 权限模型:
* getAllBooks — 无注解,游客 / 用户 / admin 均可
* getAllBorrowRecords — @RequireRole("admin"),仅管理员
*/
// @TODO:拆分到各自的服务中
@RestController
@RequestMapping("/api/dashboard")
class DashBoardController(
private val dashBoardService: DashBoardService,
private val ipExtractor: IpExtractor,
) {
/*
* GET /api/dashboard/get-all-books
* 查询全部图书列表,游客无需登录即可访问
*
* 成功响应(code=200):
* { "code":200, "message":"success", "data":[{"id":1,"name":"三体",...}, ...] }
*/
@GetMapping("/get-all-books")
fun getAllBooks(
@RequestAttribute(required = false) username: String?,
request: HttpServletRequest
): Result<Any?> {
val user = username ?: "guest"
log.info("[DashBoard] getAllBooks: user={}, ip={}", user, ipExtractor.getRealIp(request))
log.info("[DashBoard] user agent: {}", request.getHeader("User-Agent"))
return dashBoardService.getAllBooks()
}
/*
* GET /api/dashboard/admin/get-all-borrow-records
* 查询全量借阅记录(仅管理员)
*
* 成功响应(code=200):
* { "code":200, "message":"success", "data":[{"id":1,"userId":1,"bookId":2,...}, ...] }
*/
@RequireRole("admin")
@GetMapping("/admin/get-all-borrow-records")
fun getAllBorrowRecords(
@RequestAttribute(required = false) username: String?,
request: HttpServletRequest
): Result<Any?> {
log.info("[DashBoard] getAllBorrowRecords: user={}, ip={}", username ?: "unknown", ipExtractor.getRealIp(request))
log.info("[DashBoard] user agent: {}", request.getHeader("User-Agent"))
return dashBoardService.getAllBorrowRecords()
}
}
@@ -1,20 +0,0 @@
package com.msksbr.bookmgr.service
import com.msksbr.bookmgr.template.Result
/*
* 仪表盘服务接口
* 聚合图书全量查询和借阅记录全量查询
*/
interface DashBoardService {
/*
* 查询全部图书列表
* @return 包含所有图书的 JSON 列表
*/
fun getAllBooks(): Result<Any?>
/*
* 查询全部借阅记录列表
* @return 包含所有借阅记录的 JSON 列表
*/
fun getAllBorrowRecords(): Result<Any?>
}
@@ -1,5 +1,9 @@
package com.msksbr.bookmgr.service.impl package com.msksbr.bookmgr.service.impl
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper
import com.msksbr.bookmgr.entity.Book
import com.msksbr.bookmgr.entity.BorrowRecord
import com.msksbr.bookmgr.entity.User
import com.msksbr.bookmgr.mapper.BookMapper import com.msksbr.bookmgr.mapper.BookMapper
import com.msksbr.bookmgr.mapper.BorrowRecordMapper import com.msksbr.bookmgr.mapper.BorrowRecordMapper
import com.msksbr.bookmgr.mapper.UserMapper import com.msksbr.bookmgr.mapper.UserMapper
@@ -9,6 +13,10 @@ import com.msksbr.bookmgr.template.Result
import com.msksbr.bookmgr.vo.BorrowInfoVo import com.msksbr.bookmgr.vo.BorrowInfoVo
import com.msksbr.bookmgr.vo.borrow.BookBorrowVo import com.msksbr.bookmgr.vo.borrow.BookBorrowVo
import com.msksbr.bookmgr.vo.borrow.UserBorrowVo import com.msksbr.bookmgr.vo.borrow.UserBorrowVo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
/* /*
@@ -21,27 +29,120 @@ class AdminBorrowServiceImpl(
private val borrowRecordMapper: BorrowRecordMapper, private val borrowRecordMapper: BorrowRecordMapper,
private val bookMapper: BookMapper private val bookMapper: BookMapper
) : AdminBorrowService { ) : AdminBorrowService {
/*
* 搜索借阅记录:并发查询 user 表和 book 表,按用户名、角色、书名或作者匹配
* 将匹配到的 userId / bookId 取并集后查询借阅记录,降序排列
*/
override fun searchBorrows(query: String): Result<Any?> { override fun searchBorrows(query: String): Result<Any?> {
TODO("Not yet implemented") if (query.isBlank()) {
log.warn("[AdminBorrow] search: query is blank")
return Result.error("Search query cannot be empty")
}
val (matchedUserIds, matchedBookIds) = runBlocking {
val userDeferred = async(Dispatchers.IO) {
userMapper.selectList(
QueryWrapper<User>()
.like("username", query)
.or()
.like("role", query)
).map { it.id }
}
val bookDeferred = async(Dispatchers.IO) {
bookMapper.selectList(
QueryWrapper<Book>()
.like("name", query)
.or()
.like("author", query)
).map { it.id }
}
userDeferred.await() to bookDeferred.await()
}
if (matchedUserIds.isEmpty() && matchedBookIds.isEmpty()) {
log.info("[AdminBorrow] search: no results for {}", query)
return Result.notFound("No matching borrow records found")
}
val borrows = borrowRecordMapper.selectList(
QueryWrapper<BorrowRecord>()
.`in`(matchedUserIds.isNotEmpty(), "user_id", matchedUserIds)
.or()
.`in`(matchedBookIds.isNotEmpty(), "book_id", matchedBookIds)
.orderByDesc("borrow_time")
)
val userIds = borrows.map { it.userId }.distinct()
val bookIds = borrows.map { it.bookId }.distinct()
val result = runBlocking {
buildBorrowVos(borrows, userIds, bookIds)
}
log.info("[AdminBorrow] search: found {} records for {}", result.size, query)
return Result.success(result)
} }
/*
* 查询单条借阅记录,含关联的图书和用户信息
*/
override fun getOneBorrow(id: Long): Result<Any?> { override fun getOneBorrow(id: Long): Result<Any?> {
TODO("Not yet implemented") val borrow = borrowRecordMapper.selectById(id)
if (borrow == null) {
log.info("[AdminBorrow] getOne: no record for id={}", id)
return Result.notFound("Borrow record not found")
}
val user = userMapper.selectById(borrow.userId)
val book = bookMapper.selectById(borrow.bookId)
val result = BorrowInfoVo(
id = borrow.id!!,
borrowTime = borrow.borrowTime,
returnTime = borrow.returnTime,
status = borrow.status,
bookBorrowVo = BookBorrowVo(
id = book.id!!,
name = book.name,
author = book.author,
),
userBorrowVo = UserBorrowVo(
id = user.id!!,
username = user.username,
role = user.role,
)
)
log.info("[AdminBorrow] getOne: found record id={}, user={}, book={}", id, user.username, book.name)
return Result.success(result)
} }
/*
* 查询全部借阅记录,并发加载关联的用户和图书信息
*/
override fun getAllBorrows(): Result<Any?> { override fun getAllBorrows(): Result<Any?> {
val borrows = borrowRecordMapper.selectList(null) val borrows = borrowRecordMapper.selectList(null)
if (borrows.isEmpty()) { if (borrows.isEmpty()) {
log.info("[AdminBorrow] getAll: no records") log.info("[AdminBorrow] getAll: no records")
return Result.success(null) return Result.notFound("No borrow records found")
} }
val userIds = borrows.map { it.userId }.distinct() val userIds = borrows.map { it.userId }.distinct()
val bookIds = borrows.map { it.bookId }.distinct() val bookIds = borrows.map { it.bookId }.distinct()
val userMap = userMapper.selectByIds(userIds).associateBy { it.id }
val bookMap = bookMapper.selectByIds(bookIds).associateBy { it.id }
val result = borrows.map { borrow -> val result = runBlocking { buildBorrowVos(borrows, userIds, bookIds) }
log.info("[AdminBorrow] getAll: found {} records", result.size)
return Result.success(result)
}
// 并发查询用户和图书信息,组装为 BorrowInfoVo 列表
private suspend fun buildBorrowVos(
borrows: List<BorrowRecord>,
userIds: List<Long>,
bookIds: List<Long>,
): List<BorrowInfoVo> = coroutineScope {
val userMapDeferred = async(Dispatchers.IO) {
userMapper.selectByIds(userIds).associateBy { it.id }
}
val bookMapDeferred = async(Dispatchers.IO) {
bookMapper.selectByIds(bookIds).associateBy { it.id }
}
val userMap = userMapDeferred.await()
val bookMap = bookMapDeferred.await()
borrows.map { borrow ->
BorrowInfoVo( BorrowInfoVo(
id = borrow.id!!, id = borrow.id!!,
borrowTime = borrow.borrowTime, borrowTime = borrow.borrowTime,
@@ -58,13 +159,10 @@ class AdminBorrowServiceImpl(
UserBorrowVo( UserBorrowVo(
id = user.id!!, id = user.id!!,
username = user.username, username = user.username,
role = user.role role = user.role,
) )
} ?: UserBorrowVo() } ?: UserBorrowVo(),
) )
} }
log.info("[AdminBorrow] getAll: found {} records", result.size)
return Result.success(result)
} }
} }
@@ -1,51 +0,0 @@
package com.msksbr.bookmgr.service.impl
import com.msksbr.bookmgr.mapper.BookMapper
import com.msksbr.bookmgr.mapper.BorrowRecordMapper
import com.msksbr.bookmgr.script.log
import com.msksbr.bookmgr.service.DashBoardService
import com.msksbr.bookmgr.template.Result
import org.springframework.stereotype.Service
/*
* 仪表盘服务实现
* 聚合 BookMapper 和 BorrowRecordMapper 提供全量数据查询
*/
// @TODO: 拆分到book和admin borrow服务中
@Service
class DashBoardServiceImpl(
private val bookMapper: BookMapper,
private val borrowRecordMapper: BorrowRecordMapper
) : DashBoardService {
override fun getAllBooks(): Result<Any?> {
val bookList = bookMapper.selectList(null)
val bookJsonList = bookList.map { book ->
mapOf(
"id" to book.id,
"name" to book.name,
"author" to book.author,
"stock" to book.stock,
)
}
log.debug("[DashBoard] getAllBooks completed, count={}", bookJsonList.size)
return Result.success(bookJsonList)
}
// @TODO: 改成返回BorrowInfoDto
override fun getAllBorrowRecords(): Result<Any?> {
val borrowRecordList = borrowRecordMapper.selectList(null)
val borrowRecordJsonList = borrowRecordList.map { borrowRecord ->
mapOf(
"id" to borrowRecord.id,
"userId" to borrowRecord.userId,
"bookId" to borrowRecord.bookId,
"borrowTime" to borrowRecord.borrowTime,
"returnTime" to borrowRecord.returnTime,
"status" to borrowRecord.status,
)
}
log.debug("[DashBoard] getAllBorrowRecords completed, count={}", borrowRecordJsonList.size)
return Result.success(borrowRecordJsonList)
}
}