From 8f6d8eddc99b903331094a49cfd17a43ba7762f5 Mon Sep 17 00:00:00 2001 From: msksbr515 Date: Sat, 23 May 2026 00:16:03 +0800 Subject: [PATCH] feat(admin-books): implement book CRUD management endpoints - Add book with name, author, and stock validation - Update existing book information - Delete book by ID - Adjust book inventory stock - Rename AdminDashBoardService to DashBoardService - Remove hardcoded user seed data from SQL schema --- bookmgr.sql | 9 -- .../bookmgr/controller/AdminBookController.kt | 74 +++++++++++- .../bookmgr/controller/DashBoardController.kt | 12 +- .../com/msksbr/bookmgr/dto/BookAddDto.kt | 16 +++ .../com/msksbr/bookmgr/dto/BookUpdateDto.kt | 11 ++ .../bookmgr/service/AdminBookService.kt | 38 +++++++ .../bookmgr/service/AdminDashBoardService.kt | 13 --- .../bookmgr/service/DashBoardService.kt | 20 ++++ .../service/impl/AdminBookServiceImpl.kt | 105 ++++++++++++++++++ ...ServiceImpl.kt => DashBoardServiceImpl.kt} | 12 +- .../com/msksbr/bookmgr/template/Result.kt | 14 +++ 11 files changed, 286 insertions(+), 38 deletions(-) create mode 100644 src/main/kotlin/com/msksbr/bookmgr/dto/BookAddDto.kt create mode 100644 src/main/kotlin/com/msksbr/bookmgr/dto/BookUpdateDto.kt create mode 100644 src/main/kotlin/com/msksbr/bookmgr/service/AdminBookService.kt delete mode 100644 src/main/kotlin/com/msksbr/bookmgr/service/AdminDashBoardService.kt create mode 100644 src/main/kotlin/com/msksbr/bookmgr/service/DashBoardService.kt create mode 100644 src/main/kotlin/com/msksbr/bookmgr/service/impl/AdminBookServiceImpl.kt rename src/main/kotlin/com/msksbr/bookmgr/service/impl/{AdminDashBoardServiceImpl.kt => DashBoardServiceImpl.kt} (80%) diff --git a/bookmgr.sql b/bookmgr.sql index 10f4d33..7ef8a89 100644 --- a/bookmgr.sql +++ b/bookmgr.sql @@ -91,13 +91,4 @@ CREATE TABLE `user` ( UNIQUE KEY `username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; --- ---------------------------- --- Records of user --- ---------------------------- -BEGIN; -INSERT INTO `user` (`id`, `username`, `password`, `role`) VALUES (19, 'admin', '$argon2id$v=19$m=16384,t=2,p=1$D+jUHsOZ8AFqxbqDUa+PYA$QjovmIfJuhn4EV2iGV2BX9Jtz61vwGoJGa1XxnYPGqM', 'admin'); -INSERT INTO `user` (`id`, `username`, `password`, `role`) VALUES (20, 'user01', '$argon2id$v=19$m=16384,t=2,p=1$ijX5dLM4DGTj3h5y/RLjig$xuruWKz05H19D1xUbiZTC7ScgFbd9j9/VzjQ0hcYoik', 'user'); -INSERT INTO `user` (`id`, `username`, `password`, `role`) VALUES (21, 'user02', '$argon2id$v=19$m=16384,t=2,p=1$lu7jTNKaHpOY5PS5XboXgw$e+pyCrLoMJq4sQJLKQ6nzjloPrcuSmIjoeCuc+ze+mU', 'user'); -COMMIT; - SET FOREIGN_KEY_CHECKS = 1; diff --git a/src/main/kotlin/com/msksbr/bookmgr/controller/AdminBookController.kt b/src/main/kotlin/com/msksbr/bookmgr/controller/AdminBookController.kt index f8bce3c..2c84841 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/controller/AdminBookController.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/controller/AdminBookController.kt @@ -1,17 +1,83 @@ 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.dto.BookAddDto +import com.msksbr.bookmgr.dto.BookUpdateDto +import com.msksbr.bookmgr.script.log +import com.msksbr.bookmgr.service.AdminBookService +import com.msksbr.bookmgr.template.Result +import jakarta.servlet.http.HttpServletRequest +import jakarta.validation.Valid +import org.springframework.web.bind.annotation.* /* * 图书管理接口(面向管理员) -* 路径前缀(待定):/api/admin/books +* 路径前缀:/api/admin/books * -* 计划接口: +* 接口: * - 新增图书 * - 修改图书信息 * - 删除图书 * - 调整库存 +* +* 全部接口需 admin 角色,由 @RequireRole 切面校验 */ @RestController -class AdminBookController { +@RequestMapping("/api/admin/books") +class AdminBookController(private val adminBookService: AdminBookService, private val ipExtractor: IpExtractor) { + + @RequireRole("admin") + @PostMapping("/add") + fun addBook( + @Valid + @RequestBody + bookAddDto: BookAddDto, + request: HttpServletRequest, + @RequestAttribute(required = false) username: String? + ): Result { + log.info("[AdminBook] add: user={}, name={}, author={}", username ?: "guest", bookAddDto.name, bookAddDto.author) + log.info("[AdminBook] user agent: {}, ip={}", request.getHeader("User-Agent"), ipExtractor.getRealIp(request)) + return adminBookService.addBook(bookAddDto) + } + + @RequireRole("admin") + @PostMapping("/update") + fun updateBook( + id: Long, + @Valid + @RequestBody + bookUpdateDto: BookUpdateDto, + request: HttpServletRequest, + @RequestAttribute(required = false) username: String? + ): Result { + log.info("[AdminBook] update: user={}, id={}", username ?: "guest", id) + log.info("[AdminBook] user agent: {}, ip={}", request.getHeader("User-Agent"), ipExtractor.getRealIp(request)) + return adminBookService.updateBook(id, bookUpdateDto) + } + + @RequireRole("admin") + @PostMapping("/delete") + fun deleteBook( + id: Long, + request: HttpServletRequest, + @RequestAttribute(required = false) username: String? + ): Result { + log.info("[AdminBook] delete: user={}, id={}", username ?: "guest", id) + log.info("[AdminBook] user agent: {}, ip={}", request.getHeader("User-Agent"), ipExtractor.getRealIp(request)) + return adminBookService.deleteBook(id) + } + + @RequireRole("admin") + @PostMapping("/update-stock") + fun updateStock( + id: Long, + stock: Int, + request: HttpServletRequest, + @RequestAttribute(required = false) username: String? + ): Result { + log.info("[AdminBook] updateStock: user={}, id={}, stock={}", username ?: "guest", id, stock) + log.info("[AdminBook] user agent: {}, ip={}", request.getHeader("User-Agent"), ipExtractor.getRealIp(request)) + return adminBookService.updateStock(id, stock) + } } diff --git a/src/main/kotlin/com/msksbr/bookmgr/controller/DashBoardController.kt b/src/main/kotlin/com/msksbr/bookmgr/controller/DashBoardController.kt index d0c9905..4f66192 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/controller/DashBoardController.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/controller/DashBoardController.kt @@ -3,7 +3,7 @@ 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.AdminDashBoardService +import com.msksbr.bookmgr.service.DashBoardService import com.msksbr.bookmgr.template.Result import jakarta.servlet.http.HttpServletRequest import org.springframework.web.bind.annotation.GetMapping @@ -22,7 +22,7 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api/dashboard") class DashBoardController( - private val adminDashBoardService: AdminDashBoardService, + private val dashBoardService: DashBoardService, private val ipExtractor: IpExtractor, ) { /* @@ -40,7 +40,7 @@ class DashBoardController( val user = username ?: "guest" log.info("[DashBoard] getAllBooks: user={}, ip={}", user, ipExtractor.getRealIp(request)) log.info("[DashBoard] user agent: {}", request.getHeader("User-Agent")) - return adminDashBoardService.getAllBooks() + return dashBoardService.getAllBooks() } /* @@ -56,8 +56,8 @@ class DashBoardController( @RequestAttribute(required = false) username: String?, request: HttpServletRequest ): Result { - log.info("[AdminDashBoard] getAllBorrowRecords: user={}, ip={}", username ?: "unknown", ipExtractor.getRealIp(request)) - log.info("[AdminDashBoard] user agent: {}", request.getHeader("User-Agent")) - return adminDashBoardService.getAllBorrowRecords() + log.info("[DashBoard] getAllBorrowRecords: user={}, ip={}", username ?: "unknown", ipExtractor.getRealIp(request)) + log.info("[DashBoard] user agent: {}", request.getHeader("User-Agent")) + return dashBoardService.getAllBorrowRecords() } } diff --git a/src/main/kotlin/com/msksbr/bookmgr/dto/BookAddDto.kt b/src/main/kotlin/com/msksbr/bookmgr/dto/BookAddDto.kt new file mode 100644 index 0000000..4eef8f8 --- /dev/null +++ b/src/main/kotlin/com/msksbr/bookmgr/dto/BookAddDto.kt @@ -0,0 +1,16 @@ +package com.msksbr.bookmgr.dto + +import jakarta.validation.constraints.Min +import jakarta.validation.constraints.NotBlank + +/* +* 新增图书请求体 +* name — 书名,不可为空 +* author — 作者,不可为空 +* stock — 初始库存,最小值 1 +*/ +class BookAddDto( + @NotBlank val name: String, + @NotBlank val author: String, + @Min(1) val stock: Int, +) diff --git a/src/main/kotlin/com/msksbr/bookmgr/dto/BookUpdateDto.kt b/src/main/kotlin/com/msksbr/bookmgr/dto/BookUpdateDto.kt new file mode 100644 index 0000000..45b8143 --- /dev/null +++ b/src/main/kotlin/com/msksbr/bookmgr/dto/BookUpdateDto.kt @@ -0,0 +1,11 @@ +package com.msksbr.bookmgr.dto + +/* +* 修改图书请求体 +* name — 新书名,为空字符串时保留原值(但不可 name 和 author 同时为空) +* author — 新作者,为空字符串时保留原值 +*/ +data class BookUpdateDto( + val name: String, + val author: String, +) diff --git a/src/main/kotlin/com/msksbr/bookmgr/service/AdminBookService.kt b/src/main/kotlin/com/msksbr/bookmgr/service/AdminBookService.kt new file mode 100644 index 0000000..92e6a22 --- /dev/null +++ b/src/main/kotlin/com/msksbr/bookmgr/service/AdminBookService.kt @@ -0,0 +1,38 @@ +package com.msksbr.bookmgr.service + +import com.msksbr.bookmgr.dto.BookAddDto +import com.msksbr.bookmgr.dto.BookUpdateDto +import com.msksbr.bookmgr.template.Result + +/* +* 图书管理服务接口 +* 定义图书的增删改逻辑,仅由管理员调用 +*/ +interface AdminBookService { + /* + * 新增图书 + * @param bookAddDto 包含 name、author、stock + * @return 成功返回 200,重复返回 409 + */ + fun addBook(bookAddDto: BookAddDto): Result + /* + * 修改图书信息(名称或作者) + * @param id 图书 ID + * @param bookUpdateDto 包含可选的 name 和 author + * @return 成功返回 200,不存在返回 500,重复返回 409 + */ + fun updateBook(id: Long, bookUpdateDto: BookUpdateDto): Result + /* + * 删除图书 + * @param id 图书 ID + * @return 成功返回 200,不存在返回 500 + */ + fun deleteBook(id: Long): Result + /* + * 调整库存 + * @param id 图书 ID + * @param stock 新的库存数量,必须大于 0 + * @return 成功返回 200,不存在或库存不合法返回 500 + */ + fun updateStock(id: Long, stock: Int): Result +} diff --git a/src/main/kotlin/com/msksbr/bookmgr/service/AdminDashBoardService.kt b/src/main/kotlin/com/msksbr/bookmgr/service/AdminDashBoardService.kt deleted file mode 100644 index 3d184a7..0000000 --- a/src/main/kotlin/com/msksbr/bookmgr/service/AdminDashBoardService.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.msksbr.bookmgr.service - -import com.msksbr.bookmgr.template.Result - -/* -* 管理端仪表盘服务,聚合图书和借阅统计查询 -*/ -interface AdminDashBoardService { - // 查询全部图书列表 - fun getAllBooks(): Result - // 查询全部借阅记录列表 - fun getAllBorrowRecords(): Result -} \ No newline at end of file diff --git a/src/main/kotlin/com/msksbr/bookmgr/service/DashBoardService.kt b/src/main/kotlin/com/msksbr/bookmgr/service/DashBoardService.kt new file mode 100644 index 0000000..27250e7 --- /dev/null +++ b/src/main/kotlin/com/msksbr/bookmgr/service/DashBoardService.kt @@ -0,0 +1,20 @@ +package com.msksbr.bookmgr.service + +import com.msksbr.bookmgr.template.Result + +/* +* 仪表盘服务接口 +* 聚合图书全量查询和借阅记录全量查询 +*/ +interface DashBoardService { + /* + * 查询全部图书列表 + * @return 包含所有图书的 JSON 列表 + */ + fun getAllBooks(): Result + /* + * 查询全部借阅记录列表 + * @return 包含所有借阅记录的 JSON 列表 + */ + fun getAllBorrowRecords(): Result +} \ No newline at end of file diff --git a/src/main/kotlin/com/msksbr/bookmgr/service/impl/AdminBookServiceImpl.kt b/src/main/kotlin/com/msksbr/bookmgr/service/impl/AdminBookServiceImpl.kt new file mode 100644 index 0000000..6980602 --- /dev/null +++ b/src/main/kotlin/com/msksbr/bookmgr/service/impl/AdminBookServiceImpl.kt @@ -0,0 +1,105 @@ +package com.msksbr.bookmgr.service.impl + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper +import com.msksbr.bookmgr.dto.BookAddDto +import com.msksbr.bookmgr.dto.BookUpdateDto +import com.msksbr.bookmgr.entity.Book +import com.msksbr.bookmgr.mapper.BookMapper +import com.msksbr.bookmgr.script.log +import com.msksbr.bookmgr.service.AdminBookService +import com.msksbr.bookmgr.template.Result +import org.springframework.stereotype.Service + +/* +* 图书管理服务实现 +* 提供图书的新增、修改、删除和库存调整功能,按书名+作者判重 +*/ +@Service +class AdminBookServiceImpl(private val bookMapper: BookMapper) : AdminBookService { + override fun addBook(bookAddDto: BookAddDto): Result { + val book = Book( + id = null, + name = bookAddDto.name, + author = bookAddDto.author, + stock = bookAddDto.stock, + ) + if (isBookDuplicate(book)) { + log.warn("[AdminBook] add: duplicate book, name={}, author={}", book.name, book.author) + return Result.conflict("Book already exists, please update the stock instead") + } + bookMapper.insert(book) + log.info("[AdminBook] add: success, id={}, name={}", book.id, book.name) + return Result.success("Book added successfully") + } + + override fun updateBook( + id: Long, + bookUpdateDto: BookUpdateDto + ): Result { + if (bookUpdateDto.name.isBlank() && bookUpdateDto.author.isBlank()) { + log.warn("[AdminBook] update: both name and author are blank, id={}", id) + return Result.error("At least one of name or author must be provided") + } + val existing = bookMapper.selectById(id) + ?: run { + log.warn("[AdminBook] update: book not found, id={}", id) + return Result.error("Book does not exist") + } + val book = Book( + id = id, + name = bookUpdateDto.name.ifBlank { existing.name }, + author = bookUpdateDto.author.ifBlank { existing.author }, + stock = existing.stock, + ) + if (isBookDuplicate(book, book.id)) { + log.warn("[AdminBook] update: duplicate book, name={}, author={}", book.name, book.author) + return Result.conflict("Book already exists, please update the stock instead") + } + bookMapper.updateById(book) + log.info("[AdminBook] update: success, id={}", book.id) + return Result.success("Book updated successfully") + } + + override fun deleteBook(id: Long): Result { + val existing = bookMapper.selectById(id) + ?: run { + log.warn("[AdminBook] delete: book not found, id={}", id) + return Result.error("Book does not exist") + } + bookMapper.deleteById(id) + log.info("[AdminBook] delete: success, id={}, name={}", id, existing.name) + return Result.success("Book deleted successfully") + } + + override fun updateStock(id: Long, stock: Int): Result { + if (stock <= 0) { + log.warn("[AdminBook] updateStock: invalid stock={}, id={}", stock, id) + return Result.error("Stock must be greater than zero") + } + val existing = bookMapper.selectById(id) + ?: run { + log.warn("[AdminBook] updateStock: book not found, id={}", id) + return Result.error("Book does not exist") + } + val book = Book( + id = id, + name = existing.name, + author = existing.author, + stock = stock + ) + bookMapper.updateById(book) + log.debug("[AdminBook] updateStock: success, id={}, stock={}", id, stock) + return Result.success("Book updated successfully") + } + + // 查询是否有书名和作者完全相同的书 + // TODO: 后续尽量改成ISBN + private fun isBookDuplicate(book: Book, excludeId: Long? = null): Boolean { + val wrapper = QueryWrapper() + .eq("name", book.name) + .eq("author", book.author) + .apply { excludeId?.let { ne("id", it) } } + + return bookMapper.selectCount(wrapper) > 0 + } +} diff --git a/src/main/kotlin/com/msksbr/bookmgr/service/impl/AdminDashBoardServiceImpl.kt b/src/main/kotlin/com/msksbr/bookmgr/service/impl/DashBoardServiceImpl.kt similarity index 80% rename from src/main/kotlin/com/msksbr/bookmgr/service/impl/AdminDashBoardServiceImpl.kt rename to src/main/kotlin/com/msksbr/bookmgr/service/impl/DashBoardServiceImpl.kt index 34d6b14..23c363f 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/service/impl/AdminDashBoardServiceImpl.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/service/impl/DashBoardServiceImpl.kt @@ -3,19 +3,19 @@ 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.AdminDashBoardService +import com.msksbr.bookmgr.service.DashBoardService import com.msksbr.bookmgr.template.Result import org.springframework.stereotype.Service /* -* 管理端仪表盘服务实现 +* 仪表盘服务实现 * 聚合 BookMapper 和 BorrowRecordMapper 提供全量数据查询 */ @Service -class AdminDashBoardServiceImpl( +class DashBoardServiceImpl( private val bookMapper: BookMapper, private val borrowRecordMapper: BorrowRecordMapper -) : AdminDashBoardService { +) : DashBoardService { override fun getAllBooks(): Result { val bookList = bookMapper.selectList(null) val bookJsonList = bookList.map { book -> @@ -26,7 +26,7 @@ class AdminDashBoardServiceImpl( "stock" to book.stock, ) } - log.debug("[AdminDashBoard] getAllBooks completed, count={}", bookJsonList.size) + log.debug("[DashBoard] getAllBooks completed, count={}", bookJsonList.size) return Result.success(bookJsonList) } @@ -42,7 +42,7 @@ class AdminDashBoardServiceImpl( "status" to borrowRecord.status, ) } - log.debug("[AdminDashBoard] getAllBorrowRecords completed, count={}", borrowRecordJsonList.size) + log.debug("[DashBoard] getAllBorrowRecords completed, count={}", borrowRecordJsonList.size) return Result.success(borrowRecordJsonList) } } diff --git a/src/main/kotlin/com/msksbr/bookmgr/template/Result.kt b/src/main/kotlin/com/msksbr/bookmgr/template/Result.kt index d757864..c41fffd 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/template/Result.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/template/Result.kt @@ -86,5 +86,19 @@ data class Result( data = null ) } + + /* + * 资源冲突响应 — 返回 409 + 自定义提示 + * @param message 提示信息,如 "Book already exists, please update the stock instead" + * + * JSON 输出示例:{ "code": 409, "message": "xxx" } + */ + fun conflict(message: String): Result { + return Result( + code = 409, + message = message, + data = null + ) + } } }