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:
@@ -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
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user