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
This commit is contained in:
2026-05-23 00:16:03 +08:00
parent 383c17512a
commit 8f6d8eddc9
11 changed files with 286 additions and 38 deletions
-9
View File
@@ -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;
@@ -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<Any?> {
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<Any?> {
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<Any?> {
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<Any?> {
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)
}
}
@@ -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<Any?> {
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()
}
}
@@ -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,
)
@@ -0,0 +1,11 @@
package com.msksbr.bookmgr.dto
/*
* 修改图书请求体
* name — 新书名,为空字符串时保留原值(但不可 name 和 author 同时为空)
* author — 新作者,为空字符串时保留原值
*/
data class BookUpdateDto(
val name: String,
val author: String,
)
@@ -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<Any?>
/*
* 修改图书信息(名称或作者)
* @param id 图书 ID
* @param bookUpdateDto 包含可选的 name 和 author
* @return 成功返回 200,不存在返回 500,重复返回 409
*/
fun updateBook(id: Long, bookUpdateDto: BookUpdateDto): Result<Any?>
/*
* 删除图书
* @param id 图书 ID
* @return 成功返回 200,不存在返回 500
*/
fun deleteBook(id: Long): Result<Any?>
/*
* 调整库存
* @param id 图书 ID
* @param stock 新的库存数量,必须大于 0
* @return 成功返回 200,不存在或库存不合法返回 500
*/
fun updateStock(id: Long, stock: Int): Result<Any?>
}
@@ -1,13 +0,0 @@
package com.msksbr.bookmgr.service
import com.msksbr.bookmgr.template.Result
/*
* 管理端仪表盘服务,聚合图书和借阅统计查询
*/
interface AdminDashBoardService {
// 查询全部图书列表
fun getAllBooks(): Result<Any?>
// 查询全部借阅记录列表
fun getAllBorrowRecords(): Result<Any?>
}
@@ -0,0 +1,20 @@
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?>
}
@@ -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<Any?> {
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<Any?> {
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<Any?> {
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<Any?> {
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<Book>()
.eq("name", book.name)
.eq("author", book.author)
.apply { excludeId?.let { ne("id", it) } }
return bookMapper.selectCount(wrapper) > 0
}
}
@@ -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<Any?> {
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)
}
}
@@ -86,5 +86,19 @@ data class Result<T>(
data = null
)
}
/*
* 资源冲突响应 — 返回 409 + 自定义提示
* @param message 提示信息,如 "Book already exists, please update the stock instead"
*
* JSON 输出示例:{ "code": 409, "message": "xxx" }
*/
fun conflict(message: String): Result<Any?> {
return Result(
code = 409,
message = message,
data = null
)
}
}
}