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
|
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
|
@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
|
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 com.msksbr.bookmgr.service.BorrowService
|
||||||
import org.springframework.stereotype.Service
|
|
||||||
import com.msksbr.bookmgr.template.Result
|
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
|
@Service
|
||||||
class BorrowServiceImpl: BorrowService {
|
class BorrowServiceImpl(
|
||||||
|
private val borrowRecordMapper: BorrowRecordMapper,
|
||||||
|
private val bookMapper: BookMapper
|
||||||
|
) : BorrowService {
|
||||||
/*
|
/*
|
||||||
* 查询当前用户的所有借阅记录
|
* 查询当前用户的所有借阅记录
|
||||||
*/
|
*/
|
||||||
override fun getAllMyBorrows(userId: Long): Result<Any?> {
|
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,
|
query: String,
|
||||||
userId: Long
|
userId: Long
|
||||||
): Result<Any?> {
|
): 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,
|
borrowId: Long,
|
||||||
userId: Long
|
userId: Long
|
||||||
): Result<Any?> {
|
): 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(
|
override fun borrowBookForMe(
|
||||||
bookId: Long,
|
bookId: Long,
|
||||||
userId: Long
|
userId: Long
|
||||||
): Result<Any?> {
|
): 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(
|
override fun returnBookForMe(
|
||||||
borrowId: Long,
|
borrowId: Long,
|
||||||
userId: Long
|
userId: Long
|
||||||
): Result<Any?> {
|
): 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