feat(borrow): implement borrow module for regular users
- Add list, search, detail, borrow, and return endpoints to BorrowController - Implement service methods with MyBatis Plus queries and validation - Introduce MyBorrowVo response wrapper for borrow records - Add JwtPopulateAspect for populating userId from JWT claims
This commit is contained in:
@@ -1,16 +1,113 @@
|
||||
package com.msksbr.bookmgr.controller
|
||||
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import com.msksbr.bookmgr.annotation.RequireRole
|
||||
import com.msksbr.bookmgr.config.IpExtractor
|
||||
import com.msksbr.bookmgr.script.log
|
||||
import com.msksbr.bookmgr.service.BorrowService
|
||||
import com.msksbr.bookmgr.template.Result
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
/*
|
||||
* 借阅接口(面向普通用户)
|
||||
* 路径前缀(待定):/api/borrows
|
||||
* 路径前缀:/api/borrows
|
||||
*
|
||||
* 计划接口:
|
||||
* 接口:
|
||||
* - 我的借阅记录列表
|
||||
* - 借书(提交借阅请求)
|
||||
* - 搜索我的借阅记录
|
||||
* - 单条借阅详情
|
||||
* - 借书
|
||||
* - 还书
|
||||
*
|
||||
* 全部接口需 user 角色,由 @RequireRole 切面校验
|
||||
*/
|
||||
@RestController
|
||||
class BorrowController {
|
||||
@RequestMapping("/api/borrows")
|
||||
class BorrowController(
|
||||
private val borrowService: BorrowService,
|
||||
private val ipExtractor: IpExtractor
|
||||
) {
|
||||
/*
|
||||
* GET /api/borrows/getall
|
||||
* 查询当前用户的所有借阅记录
|
||||
*/
|
||||
@RequireRole("user")
|
||||
@GetMapping("/getall")
|
||||
fun getAllMyBorrows(
|
||||
@RequestAttribute(required = false) username: String?,
|
||||
@RequestAttribute(required = false) userId: Long?,
|
||||
request: HttpServletRequest
|
||||
): Result<Any?> {
|
||||
log.info("[Borrow] getAll: user={}, userId={}", username ?: "guest", userId)
|
||||
log.info("[Borrow] user agent: {}, ip={}", request.getHeader("User-Agent"), ipExtractor.getRealIp(request))
|
||||
return borrowService.getAllMyBorrows(userId!!)
|
||||
}
|
||||
|
||||
/*
|
||||
* GET /api/borrows/search?query=xxx
|
||||
* 搜索当前用户的借阅记录,按书名或作者模糊匹配
|
||||
*/
|
||||
@RequireRole("user")
|
||||
@GetMapping("/search")
|
||||
fun searchMyBorrows(
|
||||
@RequestAttribute(required = false) username: String?,
|
||||
query: String,
|
||||
@RequestAttribute(required = false) userId: Long?,
|
||||
request: HttpServletRequest
|
||||
): Result<Any?> {
|
||||
log.info("[Borrow] search: user={}, userId={}, query={}", username ?: "guest", userId, query)
|
||||
log.info("[Borrow] user agent: {}, ip={}", request.getHeader("User-Agent"), ipExtractor.getRealIp(request))
|
||||
return borrowService.searchMyBorrows(query, userId!!)
|
||||
}
|
||||
|
||||
/*
|
||||
* GET /api/borrows/getone?id=xxx
|
||||
* 查询单条借阅记录详情(仅限本人的记录)
|
||||
*/
|
||||
@RequireRole("user")
|
||||
@GetMapping("/getone")
|
||||
fun getOneMyBorrow(
|
||||
@RequestAttribute(required = false) username: String?,
|
||||
borrowId: Long,
|
||||
@RequestAttribute(required = false) userId: Long?,
|
||||
request: HttpServletRequest
|
||||
): Result<Any?> {
|
||||
log.info("[Borrow] getOne: user={}, userId={}, borrowId={}", username ?: "guest", userId, borrowId)
|
||||
log.info("[Borrow] user agent: {}, ip={}", request.getHeader("User-Agent"), ipExtractor.getRealIp(request))
|
||||
return borrowService.getOneMyBorrow(borrowId, userId!!)
|
||||
}
|
||||
|
||||
/*
|
||||
* POST /api/borrows/borrowbook?bookId=xxx
|
||||
* 借书
|
||||
*/
|
||||
@RequireRole("user")
|
||||
@PostMapping("/borrowbook")
|
||||
fun borrowBookForMe(
|
||||
@RequestAttribute(required = false) username: String?,
|
||||
bookId: Long,
|
||||
@RequestAttribute(required = false) userId: Long?,
|
||||
request: HttpServletRequest
|
||||
): Result<Any?> {
|
||||
log.info("[Borrow] borrow: user={}, userId={}, bookId={}", username ?: "guest", userId, bookId)
|
||||
log.info("[Borrow] user agent: {}, ip={}", request.getHeader("User-Agent"), ipExtractor.getRealIp(request))
|
||||
return borrowService.borrowBookForMe(bookId, userId!!)
|
||||
}
|
||||
|
||||
/*
|
||||
* POST /api/borrows/returnbook?borrowId=xxx
|
||||
* 还书(仅限本人的记录)
|
||||
*/
|
||||
@RequireRole("user")
|
||||
@PostMapping("/returnbook")
|
||||
fun returnBookForMe(
|
||||
@RequestAttribute(required = false) username: String?,
|
||||
borrowId: Long,
|
||||
@RequestAttribute(required = false) userId: Long?,
|
||||
request: HttpServletRequest
|
||||
): Result<Any?> {
|
||||
log.info("[Borrow] return: user={}, userId={}, borrowId={}", username ?: "guest", userId, borrowId)
|
||||
log.info("[Borrow] user agent: {}, ip={}", request.getHeader("User-Agent"), ipExtractor.getRealIp(request))
|
||||
return borrowService.returnBookForMe(borrowId, userId!!)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,45 @@
|
||||
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.mapper.BookMapper
|
||||
import com.msksbr.bookmgr.mapper.BorrowRecordMapper
|
||||
import com.msksbr.bookmgr.script.log
|
||||
import com.msksbr.bookmgr.service.BorrowService
|
||||
import org.springframework.stereotype.Service
|
||||
import com.msksbr.bookmgr.template.Result
|
||||
import com.msksbr.bookmgr.vo.MyBorrowVo
|
||||
import com.msksbr.bookmgr.vo.borrow.BookBorrowVo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.springframework.stereotype.Service
|
||||
import java.util.*
|
||||
|
||||
/*
|
||||
* 借阅服务实现(面向普通用户)
|
||||
*/
|
||||
@Service
|
||||
class BorrowServiceImpl: BorrowService {
|
||||
class BorrowServiceImpl(
|
||||
private val borrowRecordMapper: BorrowRecordMapper,
|
||||
private val bookMapper: BookMapper
|
||||
) : BorrowService {
|
||||
/*
|
||||
* 查询当前用户的所有借阅记录
|
||||
*/
|
||||
override fun getAllMyBorrows(userId: Long): Result<Any?> {
|
||||
TODO("Not yet implemented")
|
||||
val borrows = borrowRecordMapper.selectList(
|
||||
QueryWrapper<BorrowRecord>().eq("user_id", userId).orderByDesc("borrow_time")
|
||||
)
|
||||
if (borrows.isEmpty()) {
|
||||
log.info("[Borrow] getAll: no records for userId={}", userId)
|
||||
return Result.notFound("No borrow records found")
|
||||
}
|
||||
val bookIds = borrows.map { it.bookId }.distinct()
|
||||
val result = runBlocking { buildMyBorrowVos(borrows, bookIds) }
|
||||
log.info("[Borrow] getAll: found {} records for userId={}", result.size, userId)
|
||||
return Result.success(result)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -23,7 +49,34 @@ class BorrowServiceImpl: BorrowService {
|
||||
query: String,
|
||||
userId: Long
|
||||
): Result<Any?> {
|
||||
TODO("Not yet implemented")
|
||||
if (query.isBlank()) {
|
||||
log.warn("[Borrow] search: query is blank")
|
||||
return Result.error("Search query cannot be empty")
|
||||
}
|
||||
val matchedBookIds = bookMapper.selectList(
|
||||
QueryWrapper<Book>()
|
||||
.like("name", query)
|
||||
.or()
|
||||
.like("author", query)
|
||||
).map { it.id }
|
||||
if (matchedBookIds.isEmpty()) {
|
||||
log.info("[Borrow] search: no results for {}", query)
|
||||
return Result.notFound("No matching borrow records found")
|
||||
}
|
||||
val borrows = borrowRecordMapper.selectList(
|
||||
QueryWrapper<BorrowRecord>()
|
||||
.eq("user_id", userId)
|
||||
.`in`("book_id", matchedBookIds)
|
||||
.orderByDesc("borrow_time")
|
||||
)
|
||||
if (borrows.isEmpty()) {
|
||||
log.info("[Borrow] search: no borrows match for userId={}, query={}", userId, query)
|
||||
return Result.notFound("No matching borrow records found")
|
||||
}
|
||||
val bookIds = borrows.map { it.bookId }.distinct()
|
||||
val result = runBlocking { buildMyBorrowVos(borrows, bookIds) }
|
||||
log.info("[Borrow] search: found {} records for userId={}, query={}", result.size, userId, query)
|
||||
return Result.success(result)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -33,26 +86,139 @@ class BorrowServiceImpl: BorrowService {
|
||||
borrowId: Long,
|
||||
userId: Long
|
||||
): Result<Any?> {
|
||||
TODO("Not yet implemented")
|
||||
val borrow = borrowRecordMapper.selectById(borrowId)
|
||||
if (borrow == null) {
|
||||
log.info("[Borrow] getOne: record not found, borrowId={}", borrowId)
|
||||
return Result.notFound("Borrow record not found")
|
||||
}
|
||||
if (borrow.userId != userId) {
|
||||
log.warn(
|
||||
"[Borrow] getOne: OWNERSHIP MISMATCH — userId={} attempted to access borrowId={} belonging to userId={}, bookId={}, status={}",
|
||||
userId, borrowId, borrow.userId, borrow.bookId, borrow.status
|
||||
)
|
||||
return Result.notFound("Borrow record not found")
|
||||
}
|
||||
val book = bookMapper.selectById(borrow.bookId)
|
||||
val result = MyBorrowVo(
|
||||
id = borrow.id!!,
|
||||
borrowTime = borrow.borrowTime,
|
||||
returnTime = borrow.returnTime,
|
||||
status = borrow.status,
|
||||
bookBorrowVo = book?.let {
|
||||
BookBorrowVo(id = it.id!!, name = it.name, author = it.author)
|
||||
} ?: BookBorrowVo()
|
||||
)
|
||||
log.info("[Borrow] getOne: found record borrowId={}, userId={}, book={}", borrowId, userId, book?.name)
|
||||
return Result.success(result)
|
||||
}
|
||||
|
||||
/*
|
||||
* 借书
|
||||
* 借书(普通用户)
|
||||
*/
|
||||
override fun borrowBookForMe(
|
||||
bookId: Long,
|
||||
userId: Long
|
||||
): Result<Any?> {
|
||||
TODO("Not yet implemented")
|
||||
val matchedBook = bookMapper.selectById(bookId)
|
||||
if (matchedBook == null) {
|
||||
log.warn("[Borrow] borrow: book not found, bookId={}", bookId)
|
||||
return Result.error("Book does not exist")
|
||||
}
|
||||
if (matchedBook.stock < 1) {
|
||||
log.warn("[Borrow] borrow: book out of stock, bookId={}, stock={}", bookId, matchedBook.stock)
|
||||
return Result.conflict("Book is out of stock")
|
||||
}
|
||||
val book = Book(
|
||||
id = matchedBook.id,
|
||||
name = matchedBook.name,
|
||||
author = matchedBook.author,
|
||||
stock = matchedBook.stock - 1,
|
||||
)
|
||||
val borrow = BorrowRecord(
|
||||
id = null,
|
||||
userId = userId,
|
||||
bookId = bookId,
|
||||
borrowTime = Date(),
|
||||
returnTime = null,
|
||||
status = "BORROWED"
|
||||
)
|
||||
bookMapper.updateById(book)
|
||||
borrowRecordMapper.insert(borrow)
|
||||
log.info("[Borrow] borrow: success, userId={}, bookId={}, book={}", userId, bookId, matchedBook.name)
|
||||
return Result.success("Book borrowed successfully")
|
||||
}
|
||||
|
||||
/*
|
||||
* 还书
|
||||
* 还书(普通用户)
|
||||
*/
|
||||
override fun returnBookForMe(
|
||||
borrowId: Long,
|
||||
userId: Long
|
||||
): Result<Any?> {
|
||||
TODO("Not yet implemented")
|
||||
val matchedBorrow = borrowRecordMapper.selectById(borrowId)
|
||||
if (matchedBorrow == null) {
|
||||
log.warn("[Borrow] return: record not found, borrowId={}", borrowId)
|
||||
return Result.error("Borrow record does not exist")
|
||||
}
|
||||
if (matchedBorrow.userId != userId) {
|
||||
log.warn(
|
||||
"[Borrow] return: OWNERSHIP MISMATCH — userId={} attempted to return borrowId={} belonging to userId={}, bookId={}, status={}",
|
||||
userId, borrowId, matchedBorrow.userId, matchedBorrow.bookId, matchedBorrow.status
|
||||
)
|
||||
return Result.error("Borrow record does not exist")
|
||||
}
|
||||
if (matchedBorrow.status == "RETURNED") {
|
||||
log.warn("[Borrow] return: already returned, borrowId={}", borrowId)
|
||||
return Result.conflict("Book has already been returned")
|
||||
}
|
||||
val borrow = BorrowRecord(
|
||||
id = matchedBorrow.id,
|
||||
bookId = matchedBorrow.bookId,
|
||||
userId = matchedBorrow.userId,
|
||||
borrowTime = matchedBorrow.borrowTime,
|
||||
returnTime = Date(),
|
||||
status = "RETURNED"
|
||||
)
|
||||
val matchedBook = bookMapper.selectById(matchedBorrow.bookId)
|
||||
if (matchedBook == null) {
|
||||
log.warn("[Borrow] return: book not found, bookId={}", matchedBorrow.bookId)
|
||||
return Result.error("Book does not exist")
|
||||
}
|
||||
val book = Book(
|
||||
id = matchedBook.id,
|
||||
name = matchedBook.name,
|
||||
author = matchedBook.author,
|
||||
stock = matchedBook.stock + 1,
|
||||
)
|
||||
bookMapper.updateById(book)
|
||||
borrowRecordMapper.updateById(borrow)
|
||||
log.info("[Borrow] return: success, borrowId={}, userId={}, book={}", borrowId, userId, matchedBook.name)
|
||||
return Result.success("Book returned successfully")
|
||||
}
|
||||
|
||||
// 并发查询图书信息,组装为 MyBorrowVo 列表
|
||||
private suspend fun buildMyBorrowVos(
|
||||
borrows: List<BorrowRecord>,
|
||||
bookIds: List<Long>,
|
||||
): List<MyBorrowVo> = coroutineScope {
|
||||
val bookMap = async(Dispatchers.IO) {
|
||||
bookMapper.selectByIds(bookIds).associateBy { it.id }
|
||||
}.await()
|
||||
|
||||
borrows.map { borrow ->
|
||||
MyBorrowVo(
|
||||
id = borrow.id!!,
|
||||
borrowTime = borrow.borrowTime,
|
||||
returnTime = borrow.returnTime,
|
||||
status = borrow.status,
|
||||
bookBorrowVo = bookMap[borrow.bookId]?.let { book ->
|
||||
BookBorrowVo(
|
||||
id = book.id!!,
|
||||
name = book.name,
|
||||
author = book.author,
|
||||
)
|
||||
} ?: BookBorrowVo(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.msksbr.bookmgr.vo
|
||||
|
||||
import com.msksbr.bookmgr.vo.borrow.BookBorrowVo
|
||||
import java.util.*
|
||||
|
||||
/*
|
||||
* 用户借阅记录视图(面向普通用户)
|
||||
* 与 BorrowInfoVo 一致,但不包含其他用户的信息
|
||||
*/
|
||||
data class MyBorrowVo(
|
||||
val id: Long = 0,
|
||||
val bookBorrowVo: BookBorrowVo = BookBorrowVo(),
|
||||
val borrowTime: Date = Date(),
|
||||
val returnTime: Date? = null,
|
||||
val status: String = ""
|
||||
)
|
||||
Reference in New Issue
Block a user