diff --git a/src/main/kotlin/com/msksbr/bookmgr/controller/BorrowController.kt b/src/main/kotlin/com/msksbr/bookmgr/controller/BorrowController.kt index 2723fe2..9278089 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/controller/BorrowController.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/controller/BorrowController.kt @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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!!) + } } diff --git a/src/main/kotlin/com/msksbr/bookmgr/service/impl/BorrowServiceImpl.kt b/src/main/kotlin/com/msksbr/bookmgr/service/impl/BorrowServiceImpl.kt index 439bf43..ce10a7d 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/service/impl/BorrowServiceImpl.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/service/impl/BorrowServiceImpl.kt @@ -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 { - TODO("Not yet implemented") + val borrows = borrowRecordMapper.selectList( + QueryWrapper().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 { - 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() + .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() + .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 { - 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 { - 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 { - 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") } -} \ No newline at end of file + + // 并发查询图书信息,组装为 MyBorrowVo 列表 + private suspend fun buildMyBorrowVos( + borrows: List, + bookIds: List, + ): List = 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(), + ) + } + } +} diff --git a/src/main/kotlin/com/msksbr/bookmgr/vo/MyBorrowVo.kt b/src/main/kotlin/com/msksbr/bookmgr/vo/MyBorrowVo.kt new file mode 100644 index 0000000..4419be7 --- /dev/null +++ b/src/main/kotlin/com/msksbr/bookmgr/vo/MyBorrowVo.kt @@ -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 = "" +)