refactor(api): replace generic error responses with specific HTTP status codes

- Add ApiResult.badRequest() and ApiResult.notFound() helper methods
- Replace generic ApiResult.error() with appropriate status-specific calls
- Add ApiResultStatusAdvice for consistent response status handling
- Add KDoc comments to admin controller and service methods
This commit is contained in:
2026-05-23 22:36:33 +08:00
parent bc4f7ac8cc
commit 65a5718f9c
15 changed files with 254 additions and 38 deletions
@@ -0,0 +1,36 @@
package com.msksbr.bookmgr.config
import com.msksbr.bookmgr.template.ApiResult
import org.springframework.core.MethodParameter
import org.springframework.http.MediaType
import org.springframework.http.converter.HttpMessageConverter
import org.springframework.http.server.ServerHttpRequest
import org.springframework.http.server.ServerHttpResponse
import org.springframework.http.server.ServletServerHttpResponse
import org.springframework.web.bind.annotation.RestControllerAdvice
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice
/*
* ApiResult 状态码处理
* 将 ApiResult.code(业务状态码)映射为 HTTP 响应状态码
* 确保前端能通过 HTTP 状态码判断请求是否成功
*/
@RestControllerAdvice
class ApiResultStatusAdvice : ResponseBodyAdvice<Any> {
override fun supports(returnType: MethodParameter, converterType: Class<out HttpMessageConverter<*>>): Boolean = true
override fun beforeBodyWrite(
body: Any?,
returnType: MethodParameter,
selectedContentType: MediaType,
selectedConverterType: Class<out HttpMessageConverter<*>>,
request: ServerHttpRequest,
response: ServerHttpResponse
): Any? {
if (body is ApiResult<*>) {
(response as ServletServerHttpResponse).servletResponse.status = body.code
}
return body
}
}
@@ -29,8 +29,9 @@ class JwtPopulateFilter(
if (claims != null) { if (claims != null) {
request.setAttribute("username", claims.subject) request.setAttribute("username", claims.subject)
request.setAttribute("role", claims.get("role", String::class.java)) request.setAttribute("role", claims.get("role", String::class.java))
request.setAttribute("userId", claims.get("userId", Long::class.java)) val userId = claims.get("userId", Number::class.java)?.toLong()
log.debug("[JWT] identified: user={}, role={}, userId={}", claims.subject, claims.get("role", String::class.java), claims.get("userId", Long::class.java)) request.setAttribute("userId", userId)
log.debug("[JWT] identified: user={}, role={}, userId={}", claims.subject, claims.get("role", String::class.java), userId)
} }
} }
filterChain.doFilter(request, response) filterChain.doFilter(request, response)
@@ -35,7 +35,7 @@ class JwtUtils(
// 初始化密钥:有配置时用 SHA-256 哈希,无配置时随机生成并打印警告 // 初始化密钥:有配置时用 SHA-256 哈希,无配置时随机生成并打印警告
init { init {
val keyBytes = if (configuredSecret.isNotBlank()) { val keyBytes = if (configuredSecret.isNotBlank()) {
// 把用户提供的secret,用SHA-256哈希成256字节固定长度 // 把用户提供的secret,用SHA-256哈希成32字节固定长度
val md = MessageDigest.getInstance("SHA-256") val md = MessageDigest.getInstance("SHA-256")
md.digest(configuredSecret.toByteArray()) md.digest(configuredSecret.toByteArray())
} else { } else {
@@ -0,0 +1,70 @@
package com.msksbr.bookmgr.config
import com.msksbr.bookmgr.annotation.RequireRole
import io.swagger.v3.oas.models.Components
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.media.Content
import io.swagger.v3.oas.models.media.MediaType
import io.swagger.v3.oas.models.media.Schema
import io.swagger.v3.oas.models.responses.ApiResponse
import io.swagger.v3.oas.models.security.SecurityRequirement
import io.swagger.v3.oas.models.security.SecurityScheme
import org.springdoc.core.customizers.GlobalOperationCustomizer
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
/*
* OpenAPI 文档配置
* 配置 Swagger/OpenAPI 文档信息、JWT Bearer 认证方案以及全局错误响应定义
*
* 提供的 Bean
* - openAPI: 文档基本信息与安全方案
* - securityCustomizer: 为 @RequireRole 接口自动添加 BearerAuth 安全要求
* - errorResponseCustomizer: 为所有接口添加 400/500 响应,为受保护接口添加 401/403 响应
*/
@Configuration
class OpenApiConfig {
@Bean
fun openAPI(): OpenAPI = OpenAPI()
.components(
Components()
.addSecuritySchemes("BearerAuth",
SecurityScheme()
.name("Authorization")
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.description("JWT token returned by login endpoint")
)
)
@Bean
fun securityCustomizer(): GlobalOperationCustomizer =
GlobalOperationCustomizer { operation, handlerMethod ->
if (handlerMethod.hasMethodAnnotation(RequireRole::class.java)) {
operation.addSecurityItem(SecurityRequirement().addList("BearerAuth"))
}
operation
}
@Bean
fun errorResponseCustomizer(): GlobalOperationCustomizer =
GlobalOperationCustomizer { operation, handlerMethod ->
val isProtected = handlerMethod.hasMethodAnnotation(RequireRole::class.java)
operation.responses
.addApiResponse("400", errorResponse("Bad request — invalid parameters"))
.addApiResponse("500", errorResponse("Internal server error"))
if (isProtected) {
operation.responses
.addApiResponse("401", errorResponse("Unauthorized — missing or invalid token"))
.addApiResponse("403", errorResponse("Forbidden — insufficient permissions"))
}
operation
}
private fun errorResponse(description: String): ApiResponse =
ApiResponse().description(description)
.content(Content().addMediaType("*/*",
MediaType().schema(Schema<Any>().`$ref`("#/components/schemas/ApiResultString"))))
}
@@ -15,7 +15,7 @@ import org.springframework.security.crypto.password.PasswordEncoder
class PasswordConfig { class PasswordConfig {
/* /*
* 返回 Argon2PasswordEncoder 实例(Spring Security 5.8 默认参数) * 返回 Argon2PasswordEncoder 实例(Spring Security 5.8 默认参数)
* 参数:salt 长度 16 字节、hash 长度 32 字节、parallelism=1、memory=1<<12(4MB)、iterations=3 * 参数:salt 长度 16 字节、hash 长度 32 字节、parallelism=1、memory=1<<12(4 MiB)、iterations=3
*/ */
// 将 Argon2 的加盐哈希方法注册成 Bean // 将 Argon2 的加盐哈希方法注册成 Bean
@Bean @Bean
@@ -27,6 +27,10 @@ import org.springframework.web.bind.annotation.*
@RequestMapping("/api/admin/books") @RequestMapping("/api/admin/books")
class AdminBookController(private val adminBookService: AdminBookService, private val ipExtractor: IpExtractor) { class AdminBookController(private val adminBookService: AdminBookService, private val ipExtractor: IpExtractor) {
/*
* POST /api/admin/books/add
* 新增图书
*/
@RequireRole("admin") @RequireRole("admin")
@PostMapping("/add") @PostMapping("/add")
fun addBook( fun addBook(
@@ -46,6 +50,10 @@ class AdminBookController(private val adminBookService: AdminBookService, privat
return adminBookService.addBook(bookAddDto) return adminBookService.addBook(bookAddDto)
} }
/*
* POST /api/admin/books/update?id=xxx
* 修改图书信息
*/
@RequireRole("admin") @RequireRole("admin")
@PostMapping("/update") @PostMapping("/update")
fun updateBook( fun updateBook(
@@ -61,6 +69,10 @@ class AdminBookController(private val adminBookService: AdminBookService, privat
return adminBookService.updateBook(id, bookUpdateDto) return adminBookService.updateBook(id, bookUpdateDto)
} }
/*
* POST /api/admin/books/delete?id=xxx
* 删除图书
*/
@RequireRole("admin") @RequireRole("admin")
@PostMapping("/delete") @PostMapping("/delete")
fun deleteBook( fun deleteBook(
@@ -73,6 +85,10 @@ class AdminBookController(private val adminBookService: AdminBookService, privat
return adminBookService.deleteBook(id) return adminBookService.deleteBook(id)
} }
/*
* POST /api/admin/books/update-stock?id=xxx&stock=xxx
* 调整图书库存
*/
@RequireRole("admin") @RequireRole("admin")
@PostMapping("/update-stock") @PostMapping("/update-stock")
fun updateStock( fun updateStock(
@@ -34,8 +34,8 @@ class AuthController(
* 成功响应(code=200, message="success"): * 成功响应(code=200, message="success"):
* { "code": 200, "message": "success", "data": { "token": "...", "username": "admin", "role": "admin" } } * { "code": 200, "message": "success", "data": { "token": "...", "username": "admin", "role": "admin" } }
* *
* 失败响应(code=500, message="Incorrect username or password"): * 失败响应(code=401, message="Incorrect username or password"):
* { "code": 500, "message": "Incorrect username or password", "data": null } * { "code": 401, "message": "Incorrect username or password", "data": null }
*/ */
@PostMapping("/login") @PostMapping("/login")
fun login( fun login(
@@ -59,7 +59,7 @@ class AuthController(
ApiResult.success(loginVo) ApiResult.success(loginVo)
} else { } else {
log.warn("[Auth] login failed: user={}", loginDTO.username) log.warn("[Auth] login failed: user={}", loginDTO.username)
ApiResult.error("Incorrect username or password") ApiResult.unauthorized("Incorrect username or password")
} }
} }
@@ -22,6 +22,10 @@ import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@RequestMapping("/api/books") @RequestMapping("/api/books")
class BookController(private val bookService: BookService, private val ipExtractor: IpExtractor) { class BookController(private val bookService: BookService, private val ipExtractor: IpExtractor) {
/*
* GET /api/books/search?query=xxx
* 按书名或作者模糊搜索图书
*/
@GetMapping("/search") @GetMapping("/search")
fun searchBook( fun searchBook(
@RequestAttribute(required = false) username: String?, @RequestAttribute(required = false) username: String?,
@@ -33,6 +37,10 @@ class BookController(private val bookService: BookService, private val ipExtract
return bookService.searchBook(query) return bookService.searchBook(query)
} }
/*
* GET /api/books/getone?id=xxx
* 查询单本图书详情
*/
@GetMapping("/getone") @GetMapping("/getone")
fun getOneBook( fun getOneBook(
@RequestAttribute(required = false) username: String?, @RequestAttribute(required = false) username: String?,
@@ -44,6 +52,10 @@ class BookController(private val bookService: BookService, private val ipExtract
return bookService.getOneBook(id) return bookService.getOneBook(id)
} }
/*
* GET /api/books/getall
* 查询所有图书
*/
@GetMapping("/getall") @GetMapping("/getall")
fun getAllBooks( fun getAllBooks(
@RequestAttribute(required = false) username: String?, @RequestAttribute(required = false) username: String?,
@@ -16,6 +16,11 @@ import org.springframework.stereotype.Service
*/ */
@Service @Service
class AdminBookServiceImpl(private val bookMapper: BookMapper) : AdminBookService { class AdminBookServiceImpl(private val bookMapper: BookMapper) : AdminBookService {
/*
* 新增图书
* @param bookAddDto 图书信息(书名、作者、库存)
* @return 操作结果,重名时返回 409
*/
override fun addBook(bookAddDto: BookAddDto): ApiResult<String> { override fun addBook(bookAddDto: BookAddDto): ApiResult<String> {
val book = Book( val book = Book(
id = null, id = null,
@@ -32,18 +37,24 @@ class AdminBookServiceImpl(private val bookMapper: BookMapper) : AdminBookServic
return ApiResult.success("Book added successfully") return ApiResult.success("Book added successfully")
} }
/*
* 修改图书信息
* @param id 图书 ID
* @param bookUpdateDto 待更新的字段(书名、作者至少提供一个)
* @return 操作结果,不存在的书返回 404,重名时返回 409
*/
override fun updateBook( override fun updateBook(
id: Long, id: Long,
bookUpdateDto: BookUpdateDto bookUpdateDto: BookUpdateDto
): ApiResult<String> { ): ApiResult<String> {
if (bookUpdateDto.name.isBlank() && bookUpdateDto.author.isBlank()) { if (bookUpdateDto.name.isBlank() && bookUpdateDto.author.isBlank()) {
log.warn("[AdminBook] update: both name and author are blank, id={}", id) log.warn("[AdminBook] update: both name and author are blank, id={}", id)
return ApiResult.error("At least one of name or author must be provided") return ApiResult.badRequest("At least one of name or author must be provided")
} }
val existing = bookMapper.selectById(id) val existing = bookMapper.selectById(id)
?: run { ?: run {
log.warn("[AdminBook] update: book not found, id={}", id) log.warn("[AdminBook] update: book not found, id={}", id)
return ApiResult.error("Book does not exist") return ApiResult.notFound("Book does not exist")
} }
val book = Book( val book = Book(
id = id, id = id,
@@ -60,26 +71,37 @@ class AdminBookServiceImpl(private val bookMapper: BookMapper) : AdminBookServic
return ApiResult.success("Book updated successfully") return ApiResult.success("Book updated successfully")
} }
/*
* 删除图书
* @param id 图书 ID
* @return 操作结果,不存在的书返回 404
*/
override fun deleteBook(id: Long): ApiResult<String> { override fun deleteBook(id: Long): ApiResult<String> {
val existing = bookMapper.selectById(id) val existing = bookMapper.selectById(id)
?: run { ?: run {
log.warn("[AdminBook] delete: book not found, id={}", id) log.warn("[AdminBook] delete: book not found, id={}", id)
return ApiResult.error("Book does not exist") return ApiResult.notFound("Book does not exist")
} }
bookMapper.deleteById(id) bookMapper.deleteById(id)
log.info("[AdminBook] delete: success, id={}, name={}", id, existing.name) log.info("[AdminBook] delete: success, id={}, name={}", id, existing.name)
return ApiResult.success("Book deleted successfully") return ApiResult.success("Book deleted successfully")
} }
/*
* 调整图书库存
* @param id 图书 ID
* @param stock 新库存数量,必须大于 0
* @return 操作结果,不存在的书返回 404
*/
override fun updateStock(id: Long, stock: Int): ApiResult<String> { override fun updateStock(id: Long, stock: Int): ApiResult<String> {
if (stock <= 0) { if (stock <= 0) {
log.warn("[AdminBook] updateStock: invalid stock={}, id={}", stock, id) log.warn("[AdminBook] updateStock: invalid stock={}, id={}", stock, id)
return ApiResult.error("Stock must be greater than zero") return ApiResult.badRequest("Stock must be greater than zero")
} }
val existing = bookMapper.selectById(id) val existing = bookMapper.selectById(id)
?: run { ?: run {
log.warn("[AdminBook] updateStock: book not found, id={}", id) log.warn("[AdminBook] updateStock: book not found, id={}", id)
return ApiResult.error("Book does not exist") return ApiResult.notFound("Book does not exist")
} }
val book = Book( val book = Book(
id = id, id = id,
@@ -92,8 +114,12 @@ class AdminBookServiceImpl(private val bookMapper: BookMapper) : AdminBookServic
return ApiResult.success("Book updated successfully") return ApiResult.success("Book updated successfully")
} }
// 查询是否有书名和作者完全相同的书 /*
// TODO: 后续尽量改成ISBN * 按书名+作者判重
* @param book 待检查的图书
* @param excludeId 排除的图书 ID(更新时用于排除自身)
* @return true 表示已存在重复
*/
private fun isBookDuplicate(book: Book, excludeId: Long? = null): Boolean { private fun isBookDuplicate(book: Book, excludeId: Long? = null): Boolean {
val wrapper = QueryWrapper<Book>() val wrapper = QueryWrapper<Book>()
.eq("name", book.name) .eq("name", book.name)
@@ -37,7 +37,7 @@ class AdminBorrowServiceImpl(
override fun searchBorrows(query: String): ApiResult<List<BorrowInfoVo>> { override fun searchBorrows(query: String): ApiResult<List<BorrowInfoVo>> {
if (query.isBlank()) { if (query.isBlank()) {
log.warn("[AdminBorrow] search: query is blank") log.warn("[AdminBorrow] search: query is blank")
return ApiResult.error("Search query cannot be empty") return ApiResult.badRequest("Search query cannot be empty")
} }
val (matchedUserIds, matchedBookIds) = runBlocking { val (matchedUserIds, matchedBookIds) = runBlocking {
val userDeferred = async(Dispatchers.IO) { val userDeferred = async(Dispatchers.IO) {
@@ -143,11 +143,11 @@ class AdminBorrowServiceImpl(
} }
if (matchedUser == null) { if (matchedUser == null) {
log.warn("[AdminBorrow] borrowBook: user not found, userId={}", userId) log.warn("[AdminBorrow] borrowBook: user not found, userId={}", userId)
return ApiResult.error("User does not exist") return ApiResult.notFound("User does not exist")
} }
if (matchedBook == null) { if (matchedBook == null) {
log.warn("[AdminBorrow] borrowBook: book not found, bookId={}", bookId) log.warn("[AdminBorrow] borrowBook: book not found, bookId={}", bookId)
return ApiResult.error("Book does not exist") return ApiResult.notFound("Book does not exist")
} }
if (matchedBook.stock < 1) { if (matchedBook.stock < 1) {
log.warn("[AdminBorrow] borrowBook: book out of stock, bookId={}, stock={}", bookId, matchedBook.stock) log.warn("[AdminBorrow] borrowBook: book out of stock, bookId={}, stock={}", bookId, matchedBook.stock)
@@ -180,7 +180,7 @@ class AdminBorrowServiceImpl(
val matchedBorrow = borrowRecordMapper.selectById(recordId) val matchedBorrow = borrowRecordMapper.selectById(recordId)
if (matchedBorrow == null) { if (matchedBorrow == null) {
log.warn("[AdminBorrow] returnBook: record not found, recordId={}", recordId) log.warn("[AdminBorrow] returnBook: record not found, recordId={}", recordId)
return ApiResult.error("Borrow record does not exist") return ApiResult.notFound("Borrow record does not exist")
} }
if (matchedBorrow.status == "RETURNED") { if (matchedBorrow.status == "RETURNED") {
log.warn("[AdminBorrow] returnBook: already returned, recordId={}", recordId) log.warn("[AdminBorrow] returnBook: already returned, recordId={}", recordId)
@@ -197,7 +197,7 @@ class AdminBorrowServiceImpl(
val matchedBook = bookMapper.selectById(matchedBorrow.bookId) val matchedBook = bookMapper.selectById(matchedBorrow.bookId)
if (matchedBook == null) { if (matchedBook == null) {
log.warn("[AdminBorrow] returnBook: book not found, bookId={}", matchedBorrow.bookId) log.warn("[AdminBorrow] returnBook: book not found, bookId={}", matchedBorrow.bookId)
return ApiResult.error("Book does not exist") return ApiResult.notFound("Book does not exist")
} }
val book = Book( val book = Book(
id = matchedBook.id, id = matchedBook.id,
@@ -14,10 +14,15 @@ import org.springframework.stereotype.Service
*/ */
@Service @Service
class BookServiceImpl(private val bookMapper: BookMapper) : BookService { class BookServiceImpl(private val bookMapper: BookMapper) : BookService {
/*
* 按书名或作者模糊搜索图书
* @param query 搜索关键词,不能为空
* @return 匹配的图书列表,未匹配时返回 404
*/
override fun searchBook(query: String): ApiResult<List<Book>> { override fun searchBook(query: String): ApiResult<List<Book>> {
if (query.isBlank()) { if (query.isBlank()) {
log.warn("[Book] search: query is blank") log.warn("[Book] search: query is blank")
return ApiResult.error("Search query cannot be empty") return ApiResult.badRequest("Search query cannot be empty")
} }
val result = bookMapper.selectList( val result = bookMapper.selectList(
QueryWrapper<Book>() QueryWrapper<Book>()
@@ -33,10 +38,15 @@ class BookServiceImpl(private val bookMapper: BookMapper) : BookService {
return ApiResult.success(result) return ApiResult.success(result)
} }
/*
* 根据 ID 查询单本图书
* @param id 图书 ID,必须大于 0
* @return 图书信息,不存在时返回 404
*/
override fun getOneBook(id: Long): ApiResult<Book> { override fun getOneBook(id: Long): ApiResult<Book> {
if (id < 1) { if (id < 1) {
log.warn("[Book] getOne: invalid id={}", id) log.warn("[Book] getOne: invalid id={}", id)
return ApiResult.error("Invalid book ID") return ApiResult.badRequest("Invalid book ID")
} }
val result = bookMapper.selectOne( val result = bookMapper.selectOne(
QueryWrapper<Book>() QueryWrapper<Book>()
@@ -50,6 +60,10 @@ class BookServiceImpl(private val bookMapper: BookMapper) : BookService {
return ApiResult.success(result) return ApiResult.success(result)
} }
/*
* 查询全部图书
* @return 全部图书列表
*/
override fun getAllBooks(): ApiResult<List<Book>> { override fun getAllBooks(): ApiResult<List<Book>> {
val books = bookMapper.selectList(null) val books = bookMapper.selectList(null)
log.info("[Book] getAll: found {} books", books.size) log.info("[Book] getAll: found {} books", books.size)
@@ -51,7 +51,7 @@ class BorrowServiceImpl(
): ApiResult<List<MyBorrowVo>> { ): ApiResult<List<MyBorrowVo>> {
if (query.isBlank()) { if (query.isBlank()) {
log.warn("[Borrow] search: query is blank") log.warn("[Borrow] search: query is blank")
return ApiResult.error("Search query cannot be empty") return ApiResult.badRequest("Search query cannot be empty")
} }
val matchedBookIds = bookMapper.selectList( val matchedBookIds = bookMapper.selectList(
QueryWrapper<Book>() QueryWrapper<Book>()
@@ -122,7 +122,7 @@ class BorrowServiceImpl(
val matchedBook = bookMapper.selectById(bookId) val matchedBook = bookMapper.selectById(bookId)
if (matchedBook == null) { if (matchedBook == null) {
log.warn("[Borrow] borrow: book not found, bookId={}", bookId) log.warn("[Borrow] borrow: book not found, bookId={}", bookId)
return ApiResult.error("Book does not exist") return ApiResult.notFound("Book does not exist")
} }
if (matchedBook.stock < 1) { if (matchedBook.stock < 1) {
log.warn("[Borrow] borrow: book out of stock, bookId={}, stock={}", bookId, matchedBook.stock) log.warn("[Borrow] borrow: book out of stock, bookId={}, stock={}", bookId, matchedBook.stock)
@@ -158,14 +158,14 @@ class BorrowServiceImpl(
val matchedBorrow = borrowRecordMapper.selectById(borrowId) val matchedBorrow = borrowRecordMapper.selectById(borrowId)
if (matchedBorrow == null) { if (matchedBorrow == null) {
log.warn("[Borrow] return: record not found, borrowId={}", borrowId) log.warn("[Borrow] return: record not found, borrowId={}", borrowId)
return ApiResult.error("Borrow record does not exist") return ApiResult.notFound("Borrow record does not exist")
} }
if (matchedBorrow.userId != userId) { if (matchedBorrow.userId != userId) {
log.warn( log.warn(
"[Borrow] return: OWNERSHIP MISMATCH — userId={} attempted to return borrowId={} belonging to userId={}, bookId={}, status={}", "[Borrow] return: OWNERSHIP MISMATCH — userId={} attempted to return borrowId={} belonging to userId={}, bookId={}, status={}",
userId, borrowId, matchedBorrow.userId, matchedBorrow.bookId, matchedBorrow.status userId, borrowId, matchedBorrow.userId, matchedBorrow.bookId, matchedBorrow.status
) )
return ApiResult.error("Borrow record does not exist") return ApiResult.notFound("Borrow record does not exist")
} }
if (matchedBorrow.status == "RETURNED") { if (matchedBorrow.status == "RETURNED") {
log.warn("[Borrow] return: already returned, borrowId={}", borrowId) log.warn("[Borrow] return: already returned, borrowId={}", borrowId)
@@ -182,7 +182,7 @@ class BorrowServiceImpl(
val matchedBook = bookMapper.selectById(matchedBorrow.bookId) val matchedBook = bookMapper.selectById(matchedBorrow.bookId)
if (matchedBook == null) { if (matchedBook == null) {
log.warn("[Borrow] return: book not found, bookId={}", matchedBorrow.bookId) log.warn("[Borrow] return: book not found, bookId={}", matchedBorrow.bookId)
return ApiResult.error("Book does not exist") return ApiResult.notFound("Book does not exist")
} }
val book = Book( val book = Book(
id = matchedBook.id, id = matchedBook.id,
@@ -100,5 +100,19 @@ data class ApiResult<T>(
data = null data = null
) )
} }
/*
* 请求参数错误响应 — 返回 400 + 自定义提示
* @param message 提示信息,如 "Search query cannot be empty"
*
* JSON 输出示例:{ "code": 400, "message": "xxx" }
*/
fun <T> badRequest(message: String): ApiResult<T> {
return ApiResult(
code = 400,
message = message,
data = null
)
}
} }
} }
@@ -1,5 +1,9 @@
package com.msksbr.bookmgr.vo package com.msksbr.bookmgr.vo
/*
* 登录响应 VO
* 登录成功后返回给客户端的令牌和用户基本信息
*/
data class LoginVo( data class LoginVo(
val token: String, val token: String,
val username: String, val username: String,
+35 -12
View File
@@ -1,27 +1,50 @@
# =============================================================================
# bookMgr 主配置
# 所有敏感/环境相关的配置均通过环境变量注入
#
# 开发方式:
# 1. 复制 application-dev.yaml(已 gitignored)到同目录
# 2. 修改 DB、JWT 等配置为本地值
# 3. 启动时指定 profile: --spring.profiles.active=dev
# =============================================================================
spring: spring:
application: application:
name: bookMgr name: bookMgr
# ---- 数据源(需配置环境变量) ----
datasource: datasource:
driver-class-name: ${DB_DRIVER} driver-class-name: ${DB_DRIVER}
url: jdbc:${DB_TYPE}://${DB_URL}:${DB_PORT}/${DB_NAME} url: jdbc:${DB_TYPE}://${DB_URL}:${DB_PORT}/${DB_NAME}
username: ${DB_USER} username: ${DB_USER}
password: ${DB_PASSWORD} password: ${DB_PASSWORD}
jackson:
default-property-inclusion: non_null
property-naming-strategy: SNAKE_CASE
date-format: ${JSON_DATE_FORMAT:}
time-zone: ${JSON_TIME_ZONE:GMT}
# ---- Jackson JSON 序列化 ----
jackson:
default-property-inclusion: non_null # 序列化时忽略 null 字段
property-naming-strategy: SNAKE_CASE # 实体驼峰 → JSON snake_case
date-format: ${JSON_DATE_FORMAT:} # 日期格式,dev 配置中覆盖为 yyyy-MM-dd HH:mm:ss
time-zone: ${JSON_TIME_ZONE:GMT} # 时区,dev 配置中覆盖为 GMT+8
# ---- MyBatis-Plus ----
mybatis-plus: mybatis-plus:
configuration: configuration:
# 开启驼峰命名法 map-underscore-to-camel-case: true # 数据库下划线 → 实体驼峰映射
map-underscore-to-camel-case: true # log-impl: org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl # 调试时取消注释以输出 SQL
# 开启日志输出sql语句
# log-impl: org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl # ---- 日志 ----
logging: logging:
level: level:
com.msksbr.bookmgr: ${LOG_LEVEL:INFO} com.msksbr.bookmgr: ${LOG_LEVEL:INFO} # 包级别日志级别,dev 配置中覆盖为 DEBUG
# ---- JWT ----
jwt: jwt:
secret: ${JWT_SECRET:} secret: ${JWT_SECRET:} # 密钥(不配置则每次启动随机生成,已签发 token 失效)
expiration: ${JWT_EXPIRATION_TIME:86400000} expiration: ${JWT_EXPIRATION_TIME:86400000} # token 有效期(毫秒),默认 24 小时
# ---- SpringDoc / Swagger ----
springdoc:
api-docs:
enabled: false # 生产环境关闭,dev 配置中开启
swagger-ui:
enabled: false # 生产环境关闭,dev 配置中开启